diff --git a/include/libcamera/control_ids.h b/include/libcamera/control_ids.h
index 75b6a2d5cafe..81e3e2193767 100644
--- a/include/libcamera/control_ids.h
+++ b/include/libcamera/control_ids.h
@@ -8,34 +8,31 @@
 #ifndef __LIBCAMERA_CONTROL_IDS_H__
 #define __LIBCAMERA_CONTROL_IDS_H__
 
-#include <functional>
+#include <stdint.h>
+
+#include <libcamera/controls.h>
 
 namespace libcamera {
 
-enum ControlId {
-	AwbEnable,
-	Brightness,
-	Contrast,
-	Saturation,
-	ManualExposure,
-	ManualGain,
-};
+namespace controls {
+
+#define AWB_ENABLE		1
+#define BRIGHTNESS		2
+#define CONTRAST		3
+#define SATURATION		4
+#define MANUAL_EXPOSURE		5
+#define MANUAL_GAIN		6
+
+extern const Control<void> None;
+extern const Control<bool> AwbEnable;
+extern const Control<int32_t> Brightness;
+extern const Control<int32_t> Contrast;
+extern const Control<int32_t> Saturation;
+extern const Control<int32_t> ManualExposure;
+extern const Control<int32_t> ManualGain;
+
+} /* namespace controls */
 
 } /* namespace libcamera */
 
-namespace std {
-
-template<>
-struct hash<libcamera::ControlId> {
-	using argument_type = libcamera::ControlId;
-	using result_type = std::size_t;
-
-	result_type operator()(const argument_type &key) const noexcept
-	{
-		return std::hash<std::underlying_type<argument_type>::type>()(key);
-	}
-};
-
-} /* namespace std */
-
 #endif // __LIBCAMERA_CONTROL_IDS_H__
diff --git a/include/libcamera/controls.h b/include/libcamera/controls.h
index a3089064c4b5..9698bd1dd383 100644
--- a/include/libcamera/controls.h
+++ b/include/libcamera/controls.h
@@ -8,12 +8,9 @@
 #ifndef __LIBCAMERA_CONTROLS_H__
 #define __LIBCAMERA_CONTROLS_H__
 
-#include <stdint.h>
 #include <string>
 #include <unordered_map>
 
-#include <libcamera/control_ids.h>
-
 namespace libcamera {
 
 class Camera;
@@ -53,55 +50,75 @@ private:
 	};
 };
 
-struct ControlIdentifier {
-	ControlId id;
-	const char *name;
-	ControlType type;
+class ControlId
+{
+public:
+	unsigned int id() const { return id_; }
+	const char *name() const { return name_; }
+	ControlType type() const { return type_; }
+
+protected:
+	ControlId(unsigned int id, const char *name, ControlType type)
+		: id_(id), name_(name), type_(type)
+	{
+	}
+
+private:
+	ControlId(const ControlId &) = delete;
+	ControlId &operator=(const ControlId &) = delete;
+
+	unsigned int id_;
+	const char *name_;
+	ControlType type_;
+};
+
+static inline bool operator==(const ControlId &lhs, const ControlId &rhs)
+{
+	return lhs.id() == rhs.id();
+}
+
+static inline bool operator!=(const ControlId &lhs, const ControlId &rhs)
+{
+	return !(lhs == rhs);
+}
+
+template<typename T>
+class Control : public ControlId
+{
+public:
+	using type = T;
+
+	Control(unsigned int id, const char *name);
+
+private:
+	Control(const Control &) = delete;
+	Control &operator=(const Control &) = delete;
 };
 
 class ControlInfo
 {
 public:
-	explicit ControlInfo(ControlId id, const ControlValue &min = 0,
+	explicit ControlInfo(const ControlId &id, const ControlValue &min = 0,
 			     const ControlValue &max = 0);
 
-	ControlId id() const { return ident_->id; }
-	const char *name() const { return ident_->name; }
-	ControlType type() const { return ident_->type; }
-
+	const ControlId &id() const { return id_; }
 	const ControlValue &min() const { return min_; }
 	const ControlValue &max() const { return max_; }
 
 	std::string toString() const;
 
 private:
-	const struct ControlIdentifier *ident_;
+	const ControlId &id_;
 	ControlValue min_;
 	ControlValue max_;
 };
 
-bool operator==(const ControlInfo &lhs, const ControlInfo &rhs);
-bool operator==(const ControlId &lhs, const ControlInfo &rhs);
-bool operator==(const ControlInfo &lhs, const ControlId &rhs);
-static inline bool operator!=(const ControlInfo &lhs, const ControlInfo &rhs)
-{
-	return !(lhs == rhs);
-}
-static inline bool operator!=(const ControlId &lhs, const ControlInfo &rhs)
-{
-	return !(lhs == rhs);
-}
-static inline bool operator!=(const ControlInfo &lhs, const ControlId &rhs)
-{
-	return !(lhs == rhs);
-}
-
-using ControlInfoMap = std::unordered_map<ControlId, ControlInfo>;
+using ControlInfoMap = std::unordered_map<const ControlId *, ControlInfo>;
 
 class ControlList
 {
 private:
-	using ControlListMap = std::unordered_map<const ControlInfo *, ControlValue>;
+	using ControlListMap = std::unordered_map<const ControlId *, ControlValue>;
 
 public:
 	ControlList(Camera *camera);
@@ -114,18 +131,39 @@ public:
 	const_iterator begin() const { return controls_.begin(); }
 	const_iterator end() const { return controls_.end(); }
 
-	bool contains(ControlId id) const;
-	bool contains(const ControlInfo *info) const;
+	bool contains(const ControlId &id) const;
 	bool empty() const { return controls_.empty(); }
 	std::size_t size() const { return controls_.size(); }
 	void clear() { controls_.clear(); }
 
-	ControlValue &operator[](ControlId id);
-	ControlValue &operator[](const ControlInfo *info) { return controls_[info]; }
+	template<typename T>
+	const T &get(const Control<T> &ctrl) const
+	{
+		const ControlValue *val = find(ctrl);
+		if (!val) {
+			static T t(0);
+			return t;
+		}
+
+		return val->get<T>();
+	}
+
+	template<typename T>
+	void set(const Control<T> &ctrl, const T &value)
+	{
+		ControlValue *val = find(ctrl);
+		if (!val)
+			return;
+
+		val->set<T>(value);
+	}
 
 	void update(const ControlList &list);
 
 private:
+	const ControlValue *find(const ControlId &id) const;
+	ControlValue *find(const ControlId &id);
+
 	Camera *camera_;
 	ControlListMap controls_;
 };
diff --git a/src/libcamera/control_ids.cpp b/src/libcamera/control_ids.cpp
new file mode 100644
index 000000000000..7e4e38b572f0
--- /dev/null
+++ b/src/libcamera/control_ids.cpp
@@ -0,0 +1,72 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * control_ids.cpp : Control ID list
+ */
+
+#include <libcamera/control_ids.h>
+
+/**
+ * \file control_ids.h
+ * \brief Camera control identifiers
+ */
+
+namespace libcamera {
+
+namespace controls {
+
+/**
+ * \def AWB_ENABLE
+ * \brief Numerical value of the AwbEnable control
+ * \def BRIGHTNESS
+ * \brief Numerical value of the Brightness control
+ * \def CONTRAST
+ * \brief Numerical value of the Contrast control
+ * \def SATURATION
+ * \brief Numerical value of the Saturation control
+ * \def MANUAL_EXPOSURE
+ * \brief Numerical value of the ManualExposure control
+ * \def MANUAL_GAIN
+ * \brief Numerical value of the ManualGain control
+ */
+
+/**
+ * \brief Invalid control
+ */
+extern const Control<void> None(0, "none");
+
+/**
+ * \brief Enables or disables the AWB.
+ * \sa ManualGain
+ */
+extern const Control<bool> AwbEnable(AWB_ENABLE, "AwbEnable");
+
+/**
+ * \brief Specify a fixed brightness parameter.
+ */
+extern const Control<int32_t> Brightness(BRIGHTNESS, "Brightness");
+
+/**
+ * \brief Specify a fixed contrast parameter.
+ */
+extern const Control<int32_t> Contrast(CONTRAST, "Contrast");
+
+/**
+ * \brief Specify a fixed saturation parameter.
+ */
+extern const Control<int32_t> Saturation(SATURATION, "Saturation");
+
+/**
+ * \brief Specify a fixed exposure time in milli-seconds
+ */
+extern const Control<int32_t> ManualExposure(MANUAL_EXPOSURE, "ManualExposure");
+
+/**
+ * \brief Specify a fixed gain parameter
+ */
+extern const Control<int32_t> ManualGain(MANUAL_GAIN, "ManualGain");
+
+} /* namespace controls */
+
+} /* namespace libcamera */
diff --git a/src/libcamera/controls.cpp b/src/libcamera/controls.cpp
index 295ccd55a622..8886660b7617 100644
--- a/src/libcamera/controls.cpp
+++ b/src/libcamera/controls.cpp
@@ -18,6 +18,29 @@
 /**
  * \file controls.h
  * \brief Describes control framework and controls supported by a camera
+ *
+ * A control is a mean to govern or influence the operation of a camera. Every
+ * control is defined by a unique numerical ID, a name string and the data type
+ * of the value it stores. The libcamera API defines a set of standard controls
+ * in the libcamera::controls namespace, as a set of instances of the Control
+ * class.
+ *
+ * The main way for applications to interact with controls is through the
+ * ControlList stored in the Request class:
+ *
+ * \code{.cpp}
+ * Request *req = ...;
+ * ControlList &controls = req->controls();
+ * controls->set(controls::AwbEnable, false);
+ * controls->set(controls::ManualExposure, 1000);
+ *
+ * ...
+ *
+ * int32_t exposure = controls->get(controls::ManualExposure);
+ * \endcode
+ *
+ * The ControlList::get() and ControlList::set() methods automatically deduce
+ * the data type based on the control.
  */
 
 namespace libcamera {
@@ -173,76 +196,120 @@ std::string ControlValue::toString() const
 }
 
 /**
- * \enum ControlId
- * \brief Numerical control ID
+ * \class ControlId
+ * \brief Control static metadata
+ *
+ * The ControlId class stores a control ID, name and data type. It provides
+ * unique identification of a control, but without support for compile-time
+ * type deduction that the derived template Control class supports. See the
+ * Control class for more information.
  */
 
 /**
- * \var AwbEnable
- * ControlType: Bool
- *
- * Enables or disables the AWB. See also \a libcamera::ControlId::ManualGain
+ * \fn ControlId::ControlId(unsigned int id, const char *name, ControlType type)
+ * \brief Construct a ControlId instance
+ * \param[in] id The control numerical ID
+ * \param[in] name The control name
+ * \param[in] type The control data type
  */
 
 /**
- * \var Brightness
- * ControlType: Integer32
- *
- * Specify a fixed brightness parameter.
+ * \fn unsigned int ControlId::id() const
+ * \brief Retrieve the control numerical ID
+ * \return The control numerical ID
  */
 
 /**
- * \var Contrast
- * ControlType: Integer32
- *
- * Specify a fixed contrast parameter.
+ * \fn const char *ControlId::name() const
+ * \brief Retrieve the control name
+ * \return The control name
  */
 
 /**
- * \var Saturation
- * ControlType: Integer32
- *
- * Specify a fixed saturation parameter.
+ * \fn ControlType ControlId::type() const
+ * \brief Retrieve the control data type
+ * \return The control data type
  */
 
 /**
- * \var ManualExposure
- * ControlType: Integer32
+ * \fn bool operator==(const ControlId &lhs, const ControlId &rhs)
+ * \brief Compare two ControlId instances for equality
+ * \param[in] lhs Left-hand side ControlId
+ * \param[in] rhs Left-hand side ControlId
+ *
+ * ControlId instances are compared based on the numerical ControlId::id()
+ * only, as an object may not have two separate controls with the same
+ * numerical ID.
  *
- * Specify a fixed exposure time in milli-seconds
+ * \return True if \a lhs and \a rhs are equal, false otherwise
  */
 
 /**
- * \var ManualGain
- * ControlType: Integer32
+ * \class Control
+ * \brief Describe a control and its intrinsic properties
+ *
+ * The Control class models a control exposed by a camera. Its template type
+ * name T refers to the control data type, and allows methods that operate on
+ * control values to be defined as template methods using the same type T for
+ * the control value. See for instance how the ControlList::get() method
+ * returns a value corresponding to the type of the requested control.
+ *
+ * While this class is the main mean to refer to a control, the control
+ * identifying information are stored in the non-template base ControlId class.
+ * This allows code that operates on a set of controls of different types to
+ * reference those controls through a ControlId instead of a Control. For
+ * instance, the list of controls supported by a camera is exposed as ControlId
+ * instead of Control.
  *
- * Specify a fixed gain parameter
+ * While controls of any type can be defined through template specialisation,
+ * libcamera only supports the bool, int32_t and int64_t types natively (this
+ * includes types that are equivalent to the supported types, such as int and
+ * long int).
+ *
+ * Controls IDs shall be unique. While nothing prevents multiple instances of
+ * the Control class to be created with the same ID, this may lead to undefined
+ * behaviour.
  */
 
 /**
- * \struct ControlIdentifier
- * \brief Describe a ControlId with control specific constant meta-data
- *
- * Defines a Control with a unique ID, a name, and a type.
- * This structure is used as static part of the auto-generated control
- * definitions, which are generated from the ControlId documentation.
+ * \fn Control::Control(unsigned int id, const char *name)
+ * \brief Construct a Control instance
+ * \param[in] id The control numerical ID
+ * \param[in] name The control name
  *
- * \var ControlIdentifier::id
- * The unique ID for a control
- * \var ControlIdentifier::name
- * The string representation of the control
- * \var ControlIdentifier::type
- * The ValueType required to represent the control value
+ * The control data type is automatically deduced from the template type T.
  */
 
-/*
- * The controlTypes are automatically generated to produce a control_types.cpp
- * output. This file is not for public use, and so no suitable header exists
- * for this sole usage of the controlTypes reference. As such the extern is
- * only defined here for use during the ControlInfo constructor and should not
- * be referenced directly elsewhere.
+/**
+ * \typedef Control::type
+ * \brief The Control template type T
  */
-extern const std::unordered_map<ControlId, ControlIdentifier> controlTypes;
+
+#ifndef __DOXYGEN__
+template<>
+Control<void>::Control(unsigned int id, const char *name)
+	: ControlId(id, name, ControlTypeNone)
+{
+}
+
+template<>
+Control<bool>::Control(unsigned int id, const char *name)
+	: ControlId(id, name, ControlTypeBool)
+{
+}
+
+template<>
+Control<int32_t>::Control(unsigned int id, const char *name)
+	: ControlId(id, name, ControlTypeInteger32)
+{
+}
+
+template<>
+Control<int64_t>::Control(unsigned int id, const char *name)
+	: ControlId(id, name, ControlTypeInteger64)
+{
+}
+#endif /* __DOXYGEN__ */
 
 /**
  * \class ControlInfo
@@ -260,17 +327,10 @@ extern const std::unordered_map<ControlId, ControlIdentifier> controlTypes;
  * \param[in] min The control minimum value
  * \param[in] max The control maximum value
  */
-ControlInfo::ControlInfo(ControlId id, const ControlValue &min,
+ControlInfo::ControlInfo(const ControlId &id, const ControlValue &min,
 			 const ControlValue &max)
-	: min_(min), max_(max)
+	: id_(id), min_(min), max_(max)
 {
-	auto iter = controlTypes.find(id);
-	if (iter == controlTypes.end()) {
-		LOG(Controls, Fatal) << "Attempt to create invalid ControlInfo";
-		return;
-	}
-
-	ident_ = &iter->second;
 }
 
 /**
@@ -279,18 +339,6 @@ ControlInfo::ControlInfo(ControlId id, const ControlValue &min,
  * \return The control ID
  */
 
-/**
- * \fn ControlInfo::name()
- * \brief Retrieve the control name string
- * \return The control name string
- */
-
-/**
- * \fn ControlInfo::type()
- * \brief Retrieve the control data type
- * \return The control data type
- */
-
 /**
  * \fn ControlInfo::min()
  * \brief Retrieve the minimum value of the control
@@ -310,56 +358,11 @@ std::string ControlInfo::toString() const
 {
 	std::stringstream ss;
 
-	ss << name() << "[" << min_.toString() << ".." << max_.toString() << "]";
+	ss << id_.name() << "[" << min_.toString() << ".." << max_.toString() << "]";
 
 	return ss.str();
 }
 
-/**
- * \brief Compare control information for equality
- * \param[in] lhs Left-hand side control information
- * \param[in] rhs Right-hand side control information
- *
- * Control information is compared based on the ID only, as a camera may not
- * have two separate controls with the same ID.
- *
- * \return True if \a lhs and \a rhs are equal, false otherwise
- */
-bool operator==(const ControlInfo &lhs, const ControlInfo &rhs)
-{
-	return lhs.id() == rhs.id();
-}
-
-/**
- * \brief Compare control ID and information for equality
- * \param[in] lhs Left-hand side control identifier
- * \param[in] rhs Right-hand side control information
- *
- * Control information is compared based on the ID only, as a camera may not
- * have two separate controls with the same ID.
- *
- * \return True if \a lhs and \a rhs are equal, false otherwise
- */
-bool operator==(const ControlId &lhs, const ControlInfo &rhs)
-{
-	return lhs == rhs.id();
-}
-
-/**
- * \brief Compare control information and ID for equality
- * \param[in] lhs Left-hand side control information
- * \param[in] rhs Right-hand side control identifier
- *
- * Control information is compared based on the ID only, as a camera may not
- * have two separate controls with the same ID.
- *
- * \return True if \a lhs and \a rhs are equal, false otherwise
- */
-bool operator==(const ControlInfo &lhs, const ControlId &rhs)
-{
-	return lhs.id() == rhs;
-}
-
 /**
  * \typedef ControlInfoMap
  * \brief A map of ControlId to ControlInfo
@@ -431,29 +434,9 @@ ControlList::ControlList(Camera *camera)
  *
  * \return True if the list contains a matching control, false otherwise
  */
-bool ControlList::contains(ControlId id) const
+bool ControlList::contains(const ControlId &id) const
 {
-	const ControlInfoMap &controls = camera_->controls();
-	const auto iter = controls.find(id);
-	if (iter == controls.end()) {
-		LOG(Controls, Error)
-			<< "Camera " << camera_->name()
-			<< " does not support control " << id;
-
-		return false;
-	}
-
-	return controls_.find(&iter->second) != controls_.end();
-}
-
-/**
- * \brief Check if the list contains a control with the specified \a info
- * \param[in] info The control info
- * \return True if the list contains a matching control, false otherwise
- */
-bool ControlList::contains(const ControlInfo *info) const
-{
-	return controls_.find(info) != controls_.end();
+	return controls_.find(&id) != controls_.end();
 }
 
 /**
@@ -474,44 +457,60 @@ bool ControlList::contains(const ControlInfo *info) const
  */
 
 /**
- * \brief Access or insert the control specified by \a id
- * \param[in] id The control ID
+ * \fn template<typename T> const T &ControlList::get() const
+ * \brief Get the value of a control
+ * \param[in] ctrl The control
  *
- * This method returns a reference to the control identified by \a id, inserting
- * it in the list if the ID is not already present.
+ * The behaviour is undefined if the control \a ctrl is not present in the
+ * list. Use ControlList::contains() to test for the presence of a control in
+ * the list before retrieving its value.
  *
- * The behaviour is undefined if the control \a id is not supported by the
- * camera that the ControlList refers to.
+ * The control value type shall match the type T, otherwise the behaviour is
+ * undefined.
  *
- * \return A reference to the value of the control identified by \a id
+ * \return The control value
  */
-ControlValue &ControlList::operator[](ControlId id)
+
+/**
+ * \fn template<typename T> void ControlList::set()
+ * \brief Set the control value to \a value
+ * \param[in] ctrl The control
+ * \param[in] value The control value
+ *
+ * This method sets the value of a control in the control list. If the control
+ * is already present in the list, its value is updated.
+ *
+ * The behaviour is undefined if the control \a ctrl is not supported by the
+ * camera that the list refers to.
+ */
+
+const ControlValue *ControlList::find(const ControlId &id) const
+{
+	const auto iter = controls_.find(&id);
+	if (iter == controls_.end()) {
+		LOG(Controls, Error)
+			<< "Control " << id.name() << " not found";
+
+		return nullptr;
+	}
+
+	return &iter->second;
+}
+
+ControlValue *ControlList::find(const ControlId &id)
 {
 	const ControlInfoMap &controls = camera_->controls();
-	const auto iter = controls.find(id);
+	const auto iter = controls.find(&id);
 	if (iter == controls.end()) {
 		LOG(Controls, Error)
 			<< "Camera " << camera_->name()
-			<< " does not support control " << id;
-
-		static ControlValue empty;
-		return empty;
+			<< " does not support control " << id.name();
+		return nullptr;
 	}
 
-	return controls_[&iter->second];
+	return &controls_[&id];
 }
 
-/**
- * \fn ControlList::operator[](const ControlInfo *info)
- * \brief Access or insert the control specified by \a info
- * \param[in] info The control info
- *
- * This method returns a reference to the control identified by \a info,
- * inserting it in the list if the info is not already present.
- *
- * \return A reference to the value of the control identified by \a info
- */
-
 /**
  * \brief Update the list with a union of itself and \a other
  * \param other The other list
@@ -533,10 +532,10 @@ void ControlList::update(const ControlList &other)
 	}
 
 	for (auto it : other) {
-		const ControlInfo *info = it.first;
+		const ControlId *id = it.first;
 		const ControlValue &value = it.second;
 
-		controls_[info] = value;
+		controls_[id] = value;
 	}
 }
 
diff --git a/src/libcamera/gen-controls.awk b/src/libcamera/gen-controls.awk
deleted file mode 100755
index a3f291e7071c..000000000000
--- a/src/libcamera/gen-controls.awk
+++ /dev/null
@@ -1,106 +0,0 @@
-#!/usr/bin/awk -f
-
-# SPDX-License-Identifier: LGPL-2.1-or-later
-
-# Controls are documented using Doxygen in the main controls.cpp source.
-#
-# Generate control tables directly from the documentation, creating enumerations
-# to support the IDs and static type information regarding each control.
-
-BEGIN {
-	id=0
-	input=ARGV[1]
-	mode=ARGV[2]
-	output=ARGV[3]
-}
-
-# Detect Doxygen style comment blocks and ignore other lines
-/^\/\*\*$/ { in_doxygen=1; first_line=1; next }
-// { if (!in_doxygen) next }
-
-# Entry point for the Control Documentation
-/ * \\enum ControlId$/ { in_controls=1; first_line=0; next }
-// { if (!in_controls) next }
-
-# Extract control information
-/ \* \\var/ { names[++id]=$3; first_line=0; next }
-/ \* ControlType:/ { types[id] = $3 }
-
-# End of comment blocks
-/^ \*\// { in_doxygen=0 }
-
-# Identify the end of controls
-/^ \* \\/ { if (first_line) exit }
-// { first_line=0 }
-
-################################
-# Support output file generation
-
-function basename(file) {
-	sub(".*/", "", file)
-	return file
-}
-
-function Header(file, description) {
-	print "/* SPDX-License-Identifier: LGPL-2.1-or-later */" > file
-	print "/*" > file
-	print " * Copyright (C) 2019, Google Inc." > file
-	print " *" > file
-	print " * " basename(file) " - " description > file
-	print " *" > file
-	print " * This file is auto-generated. Do not edit." > file
-	print " */" > file
-	print "" > file
-}
-
-function EnterNameSpace(file) {
-	print "namespace libcamera {" > file
-	print "" > file
-}
-
-function ExitNameSpace(file) {
-	print "" > file
-	print "} /* namespace libcamera */" > file
-}
-
-function GenerateHeader(file) {
-	Header(file, "Control ID list")
-
-	print "#ifndef __LIBCAMERA_CONTROL_IDS_H__" > file
-	print "#define __LIBCAMERA_CONTROL_IDS_H__" > file
-	print "" > file
-
-	EnterNameSpace(file)
-	print "enum ControlId {" > file
-	for (i=1; i <= id; ++i) {
-		printf "\t%s,\n", names[i] > file
-	}
-	print "};" > file
-	ExitNameSpace(file)
-
-	print "" > file
-	print "#endif // __LIBCAMERA_CONTROL_IDS_H__" > file
-}
-
-function GenerateTable(file) {
-	Header(file, "Control types")
-	print "#include <libcamera/controls.h>" > file
-	print "" > file
-
-	EnterNameSpace(file)
-
-	print "extern const std::unordered_map<ControlId, ControlIdentifier>" > file
-	print "controlTypes {" > file
-	for (i=1; i <= id; ++i) {
-		printf "\t{ %s, { %s, \"%s\", ControlType%s } },\n", names[i], names[i], names[i], types[i] > file
-	}
-	print "};" > file
-	ExitNameSpace(file)
-}
-
-END {
-	if (mode == "--header")
-		GenerateHeader(output)
-	else
-		GenerateTable(output)
-}
diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
index 755149c55c7b..8123d1d5bee9 100644
--- a/src/libcamera/meson.build
+++ b/src/libcamera/meson.build
@@ -5,6 +5,7 @@ libcamera_sources = files([
     'camera_manager.cpp',
     'camera_sensor.cpp',
     'controls.cpp',
+    'control_ids.cpp',
     'device_enumerator.cpp',
     'device_enumerator_sysfs.cpp',
     'event_dispatcher.cpp',
@@ -57,16 +58,6 @@ if libudev.found()
     ])
 endif
 
-gen_controls = files('gen-controls.awk')
-
-control_types_cpp = custom_target('control_types_cpp',
-                                  input : files('controls.cpp'),
-                                  output : 'control_types.cpp',
-                                  depend_files : gen_controls,
-                                  command : [gen_controls, '@INPUT@', '--code', '@OUTPUT@'])
-
-libcamera_sources += control_types_cpp
-
 gen_version = join_paths(meson.source_root(), 'utils', 'gen-version.sh')
 
 version_cpp = vcs_tag(command : [gen_version, meson.build_root()],
diff --git a/src/libcamera/pipeline/uvcvideo.cpp b/src/libcamera/pipeline/uvcvideo.cpp
index 0d56758e3e1d..44e19f40f1b9 100644
--- a/src/libcamera/pipeline/uvcvideo.cpp
+++ b/src/libcamera/pipeline/uvcvideo.cpp
@@ -10,6 +10,7 @@
 #include <tuple>
 
 #include <libcamera/camera.h>
+#include <libcamera/control_ids.h>
 #include <libcamera/controls.h>
 #include <libcamera/request.h>
 #include <libcamera/stream.h>
@@ -230,33 +231,20 @@ int PipelineHandlerUVC::processControls(UVCCameraData *data, Request *request)
 	V4L2ControlList controls;
 
 	for (auto it : request->controls()) {
-		const ControlInfo *ci = it.first;
+		const ControlId &id = *it.first;
 		ControlValue &value = it.second;
 
-		switch (ci->id()) {
-		case Brightness:
+		if (id == controls::Brightness) {
 			controls.add(V4L2_CID_BRIGHTNESS, value.get<int32_t>());
-			break;
-
-		case Contrast:
+		} else if (id == controls::Contrast) {
 			controls.add(V4L2_CID_CONTRAST, value.get<int32_t>());
-			break;
-
-		case Saturation:
+		} else if (id == controls::Saturation) {
 			controls.add(V4L2_CID_SATURATION, value.get<int32_t>());
-			break;
-
-		case ManualExposure:
+		} else if (id == controls::ManualExposure) {
 			controls.add(V4L2_CID_EXPOSURE_AUTO, 1);
 			controls.add(V4L2_CID_EXPOSURE_ABSOLUTE, value.get<int32_t>());
-			break;
-
-		case ManualGain:
+		} else if (id == controls::ManualGain) {
 			controls.add(V4L2_CID_GAIN, value.get<int32_t>());
-			break;
-
-		default:
-			break;
 		}
 	}
 
@@ -352,23 +340,23 @@ int UVCCameraData::init(MediaEntity *entity)
 	for (const auto &ctrl : controls) {
 		unsigned int v4l2Id = ctrl.first;
 		const V4L2ControlInfo &info = ctrl.second;
-		ControlId id;
+		const ControlId *id = &controls::None;
 
 		switch (v4l2Id) {
 		case V4L2_CID_BRIGHTNESS:
-			id = Brightness;
+			id = &controls::Brightness;
 			break;
 		case V4L2_CID_CONTRAST:
-			id = Contrast;
+			id = &controls::Contrast;
 			break;
 		case V4L2_CID_SATURATION:
-			id = Saturation;
+			id = &controls::Saturation;
 			break;
 		case V4L2_CID_EXPOSURE_ABSOLUTE:
-			id = ManualExposure;
+			id = &controls::ManualExposure;
 			break;
 		case V4L2_CID_GAIN:
-			id = ManualGain;
+			id = &controls::ManualGain;
 			break;
 		default:
 			continue;
@@ -376,7 +364,7 @@ int UVCCameraData::init(MediaEntity *entity)
 
 		controlInfo_.emplace(std::piecewise_construct,
 				     std::forward_as_tuple(id),
-				     std::forward_as_tuple(id, info.min(), info.max()));
+				     std::forward_as_tuple(*id, info.min(), info.max()));
 	}
 
 	return 0;
diff --git a/src/libcamera/pipeline/vimc.cpp b/src/libcamera/pipeline/vimc.cpp
index e549dc64b996..a48febf2172e 100644
--- a/src/libcamera/pipeline/vimc.cpp
+++ b/src/libcamera/pipeline/vimc.cpp
@@ -15,6 +15,7 @@
 #include <ipa/ipa_interface.h>
 #include <ipa/ipa_module_info.h>
 #include <libcamera/camera.h>
+#include <libcamera/control_ids.h>
 #include <libcamera/controls.h>
 #include <libcamera/request.h>
 #include <libcamera/stream.h>
@@ -283,24 +284,15 @@ int PipelineHandlerVimc::processControls(VimcCameraData *data, Request *request)
 	V4L2ControlList controls;
 
 	for (auto it : request->controls()) {
-		const ControlInfo *ci = it.first;
+		const ControlId &id = *it.first;
 		ControlValue &value = it.second;
 
-		switch (ci->id()) {
-		case Brightness:
+		if (id == controls::Brightness) {
 			controls.add(V4L2_CID_BRIGHTNESS, value.get<int32_t>());
-			break;
-
-		case Contrast:
+		} else if (id == controls::Contrast) {
 			controls.add(V4L2_CID_CONTRAST, value.get<int32_t>());
-			break;
-
-		case Saturation:
+		} else if (id == controls::Saturation) {
 			controls.add(V4L2_CID_SATURATION, value.get<int32_t>());
-			break;
-
-		default:
-			break;
 		}
 	}
 
@@ -427,17 +419,17 @@ int VimcCameraData::init(MediaDevice *media)
 	for (const auto &ctrl : controls) {
 		unsigned int v4l2Id = ctrl.first;
 		const V4L2ControlInfo &info = ctrl.second;
-		ControlId id;
+		const ControlId *id = &controls::None;
 
 		switch (v4l2Id) {
 		case V4L2_CID_BRIGHTNESS:
-			id = Brightness;
+			id = &controls::Brightness;
 			break;
 		case V4L2_CID_CONTRAST:
-			id = Contrast;
+			id = &controls::Contrast;
 			break;
 		case V4L2_CID_SATURATION:
-			id = Saturation;
+			id = &controls::Saturation;
 			break;
 		default:
 			continue;
@@ -445,7 +437,7 @@ int VimcCameraData::init(MediaDevice *media)
 
 		controlInfo_.emplace(std::piecewise_construct,
 				     std::forward_as_tuple(id),
-				     std::forward_as_tuple(id, info.min(), info.max()));
+				     std::forward_as_tuple(*id, info.min(), info.max()));
 	}
 
 	return 0;
diff --git a/test/controls/control_info.cpp b/test/controls/control_info.cpp
index 2aba568a302e..dbc43df8e3d3 100644
--- a/test/controls/control_info.cpp
+++ b/test/controls/control_info.cpp
@@ -7,6 +7,7 @@
 
 #include <iostream>
 
+#include <libcamera/control_ids.h>
 #include <libcamera/controls.h>
 
 #include "test.h"
@@ -23,17 +24,17 @@ protected:
 		 * Test information retrieval from a control with no minimum
 		 * and maximum.
 		 */
-		ControlInfo info(Brightness);
+		ControlInfo brightness(controls::Brightness);
 
-		if (info.id() != Brightness ||
-		    info.type() != ControlTypeInteger32 ||
-		    info.name() != std::string("Brightness")) {
+		if (brightness.id() != controls::Brightness ||
+		    brightness.id().type() != ControlTypeInteger32 ||
+		    brightness.id().name() != std::string("Brightness")) {
 			cout << "Invalid control identification for Brightness" << endl;
 			return TestFail;
 		}
 
-		if (info.min().get<int32_t>() != 0 ||
-		    info.max().get<int32_t>() != 0) {
+		if (brightness.min().get<int32_t>() != 0 ||
+		    brightness.max().get<int32_t>() != 0) {
 			cout << "Invalid control range for Brightness" << endl;
 			return TestFail;
 		}
@@ -42,17 +43,17 @@ protected:
 		 * Test information retrieval from a control with a minimum and
 		 * a maximum value.
 		 */
-		info = ControlInfo(Contrast, 10, 200);
+		ControlInfo contrast(controls::Contrast, 10, 200);
 
-		if (info.id() != Contrast ||
-		    info.type() != ControlTypeInteger32 ||
-		    info.name() != std::string("Contrast")) {
+		if (contrast.id() != controls::Contrast ||
+		    contrast.id().type() != ControlTypeInteger32 ||
+		    contrast.id().name() != std::string("Contrast")) {
 			cout << "Invalid control identification for Contrast" << endl;
 			return TestFail;
 		}
 
-		if (info.min().get<int32_t>() != 10 ||
-		    info.max().get<int32_t>() != 200) {
+		if (contrast.min().get<int32_t>() != 10 ||
+		    contrast.max().get<int32_t>() != 200) {
 			cout << "Invalid control range for Contrast" << endl;
 			return TestFail;
 		}
diff --git a/test/controls/control_list.cpp b/test/controls/control_list.cpp
index 5215840b1c4e..053696814b67 100644
--- a/test/controls/control_list.cpp
+++ b/test/controls/control_list.cpp
@@ -9,6 +9,7 @@
 
 #include <libcamera/camera.h>
 #include <libcamera/camera_manager.h>
+#include <libcamera/control_ids.h>
 #include <libcamera/controls.h>
 
 #include "test.h"
@@ -52,7 +53,7 @@ protected:
 			return TestFail;
 		}
 
-		if (list.contains(Brightness)) {
+		if (list.contains(controls::Brightness)) {
 			cout << "List should not contain Brightness control" << endl;
 			return TestFail;
 		}
@@ -70,7 +71,7 @@ protected:
 		 * Set a control, and verify that the list now contains it, and
 		 * nothing else.
 		 */
-		list[Brightness] = 255;
+		list.set(controls::Brightness, 255);
 
 		if (list.empty()) {
 			cout << "List should not be empty" << endl;
@@ -82,7 +83,7 @@ protected:
 			return TestFail;
 		}
 
-		if (!list.contains(Brightness)) {
+		if (!list.contains(controls::Brightness)) {
 			cout << "List should contain Brightness control" << endl;
 			return TestFail;
 		}
@@ -96,54 +97,45 @@ protected:
 			return TestFail;
 		}
 
-		if (list[Brightness].get<int32_t>() != 255) {
+		if (list.get(controls::Brightness) != 255) {
 			cout << "Incorrest Brightness control value" << endl;
 			return TestFail;
 		}
 
-		if (list.contains(Contrast)) {
+		if (list.contains(controls::Contrast)) {
 			cout << "List should not contain Contract control" << endl;
 			return TestFail;
 		}
 
-		/*
-		 * Set a second control through ControlInfo and retrieve it
-		 * through both controlId and ControlInfo.
-		 */
-		const ControlInfoMap &controls = camera_->controls();
-		const ControlInfo *brightness = &controls.find(Brightness)->second;
-		const ControlInfo *contrast = &controls.find(Contrast)->second;
+		/* Update the first control and set a second one. */
+		list.set(controls::Brightness, 64);
+		list.set(controls::Contrast, 128);
 
-		list[brightness] = 64;
-		list[contrast] = 128;
-
-		if (!list.contains(Contrast) || !list.contains(contrast)) {
+		if (!list.contains(controls::Contrast) ||
+		    !list.contains(controls::Contrast)) {
 			cout << "List should contain Contrast control" << endl;
 			return TestFail;
 		}
 
-		/*
-		 * Test control value retrieval and update through ControlInfo.
-		 */
-		if (list[brightness].get<int32_t>() != 64 ||
-		    list[contrast].get<int32_t>() != 128) {
+		if (list.get(controls::Brightness) != 64 ||
+		    list.get(controls::Contrast) != 128) {
 			cout << "Failed to retrieve control value" << endl;
 			return TestFail;
 		}
 
-		list[brightness] = 10;
-		list[contrast] = 20;
+		/*
+		 * Update both controls and verify that the container doesn't
+		 * grow.
+		 */
+		list.set(controls::Brightness, 10);
+		list.set(controls::Contrast, 20);
 
-		if (list[brightness].get<int32_t>() != 10 ||
-		    list[contrast].get<int32_t>() != 20) {
+		if (list.get(controls::Brightness) != 10 ||
+		    list.get(controls::Contrast) != 20) {
 			cout << "Failed to update control value" << endl;
 			return TestFail;
 		}
 
-		/*
-		 * Assert that the container has not grown with the control
-		 * updated.
-		 */
 		if (list.size() != 2) {
 			cout << "List should contain two elements" << endl;
 			return TestFail;
@@ -157,8 +149,8 @@ protected:
 		 */
 		ControlList newList(camera_.get());
 
-		newList[Brightness] = 128;
-		newList[Saturation] = 255;
+		newList.set(controls::Brightness, 128);
+		newList.set(controls::Saturation, 255);
 		newList.update(list);
 
 		list.clear();
@@ -178,16 +170,16 @@ protected:
 			return TestFail;
 		}
 
-		if (!newList.contains(Brightness) ||
-		    !newList.contains(Contrast) ||
-		    !newList.contains(Saturation)) {
+		if (!newList.contains(controls::Brightness) ||
+		    !newList.contains(controls::Contrast) ||
+		    !newList.contains(controls::Saturation)) {
 			cout << "New list contains incorrect items" << endl;
 			return TestFail;
 		}
 
-		if (newList[Brightness].get<int32_t>() != 10 ||
-		    newList[Contrast].get<int32_t>() != 20 ||
-		    newList[Saturation].get<int32_t>() != 255) {
+		if (newList.get(controls::Brightness) != 10 ||
+		    newList.get(controls::Contrast) != 20 ||
+		    newList.get(controls::Saturation) != 255) {
 			cout << "New list contains incorrect values" << endl;
 			return TestFail;
 		}
