diff --git a/src/ipa/ipu3/ipu3.cpp b/src/ipa/ipu3/ipu3.cpp
index b11b03efa6ce..5ee3fd302e01 100644
--- a/src/ipa/ipu3/ipu3.cpp
+++ b/src/ipa/ipu3/ipu3.cpp
@@ -11,7 +11,6 @@
 #include <sys/mman.h>
 
 #include <linux/intel-ipu3.h>
-#include <linux/v4l2-controls.h>
 
 #include <libcamera/buffer.h>
 #include <libcamera/control_ids.h>
@@ -23,6 +22,7 @@
 
 #include "libcamera/internal/buffer.h"
 #include "libcamera/internal/log.h"
+#include "libcamera/internal/v4l2_controls.h"
 
 namespace libcamera {
 
@@ -80,13 +80,13 @@ void IPAIPU3::configure([[maybe_unused]] const CameraSensorInfo &info,
 
 	ctrls_ = entityControls.at(0);
 
-	const auto itExp = ctrls_.find(V4L2_CID_EXPOSURE);
+	const auto itExp = ctrls_.find(&v4l2::EXPOSURE);
 	if (itExp == ctrls_.end()) {
 		LOG(IPAIPU3, Error) << "Can't find exposure control";
 		return;
 	}
 
-	const auto itGain = ctrls_.find(V4L2_CID_ANALOGUE_GAIN);
+	const auto itGain = ctrls_.find(&v4l2::ANALOGUE_GAIN);
 	if (itGain == ctrls_.end()) {
 		LOG(IPAIPU3, Error) << "Can't find gain control";
 		return;
@@ -214,8 +214,8 @@ void IPAIPU3::setControls(unsigned int frame)
 	op.operation = IPU3_IPA_ACTION_SET_SENSOR_CONTROLS;
 
 	ControlList ctrls(ctrls_);
-	ctrls.set(V4L2_CID_EXPOSURE, static_cast<int32_t>(exposure_));
-	ctrls.set(V4L2_CID_ANALOGUE_GAIN, static_cast<int32_t>(gain_));
+	ctrls.set(v4l2::EXPOSURE, exposure_);
+	ctrls.set(v4l2::ANALOGUE_GAIN, gain_);
 	op.controls.push_back(ctrls);
 
 	queueFrameAction.emit(frame, op);
diff --git a/src/ipa/raspberrypi/raspberrypi.cpp b/src/ipa/raspberrypi/raspberrypi.cpp
index ff14cfc4b706..897f3f0b91f1 100644
--- a/src/ipa/raspberrypi/raspberrypi.cpp
+++ b/src/ipa/raspberrypi/raspberrypi.cpp
@@ -542,16 +542,16 @@ void IPARPi::reportMetadata()
 
 bool IPARPi::validateSensorControls()
 {
-	static const uint32_t ctrls[] = {
-		V4L2_CID_ANALOGUE_GAIN,
-		V4L2_CID_EXPOSURE,
-		V4L2_CID_VBLANK,
+	static const ControlId *const ctrls[] = {
+		&v4l2::ANALOGUE_GAIN,
+		&v4l2::EXPOSURE,
+		&v4l2::VBLANK,
 	};
 
 	for (auto c : ctrls) {
 		if (sensorCtrls_.find(c) == sensorCtrls_.end()) {
 			LOG(IPARPI, Error) << "Unable to find sensor control "
-					   << utils::hex(c);
+					   << c->name();
 			return false;
 		}
 	}
@@ -1038,10 +1038,8 @@ void IPARPi::applyAWB(const struct AwbStatus *awbStatus, ControlList &ctrls)
 	LOG(IPARPI, Debug) << "Applying WB R: " << awbStatus->gain_r << " B: "
 			   << awbStatus->gain_b;
 
-	ctrls.set(V4L2_CID_RED_BALANCE,
-		  static_cast<int32_t>(awbStatus->gain_r * 1000));
-	ctrls.set(V4L2_CID_BLUE_BALANCE,
-		  static_cast<int32_t>(awbStatus->gain_b * 1000));
+	ctrls.set(v4l2::RED_BALANCE, awbStatus->gain_r * 1000);
+	ctrls.set(v4l2::BLUE_BALANCE, awbStatus->gain_b * 1000);
 }
 
 void IPARPi::applyFrameDurations(double minFrameDuration, double maxFrameDuration)
@@ -1100,15 +1098,14 @@ void IPARPi::applyAGC(const struct AgcStatus *agcStatus, ControlList &ctrls)
 	 * exposure time without us knowing. The next time though this function should
 	 * clip exposure correctly.
 	 */
-	ctrls.set(V4L2_CID_VBLANK, vblanking);
-	ctrls.set(V4L2_CID_EXPOSURE, exposureLines);
-	ctrls.set(V4L2_CID_ANALOGUE_GAIN, gainCode);
+	ctrls.set(v4l2::VBLANK, vblanking);
+	ctrls.set(v4l2::EXPOSURE, exposureLines);
+	ctrls.set(v4l2::ANALOGUE_GAIN, gainCode);
 }
 
 void IPARPi::applyDG(const struct AgcStatus *dgStatus, ControlList &ctrls)
 {
-	ctrls.set(V4L2_CID_DIGITAL_GAIN,
-		  static_cast<int32_t>(dgStatus->digital_gain * 1000));
+	ctrls.set(v4l2::DIGITAL_GAIN, dgStatus->digital_gain * 1000);
 }
 
 void IPARPi::applyCCM(const struct CcmStatus *ccmStatus, ControlList &ctrls)
diff --git a/src/ipa/rkisp1/rkisp1.cpp b/src/ipa/rkisp1/rkisp1.cpp
index 39783abd731b..ce47ba8cb38a 100644
--- a/src/ipa/rkisp1/rkisp1.cpp
+++ b/src/ipa/rkisp1/rkisp1.cpp
@@ -13,7 +13,6 @@
 #include <sys/mman.h>
 
 #include <linux/rkisp1-config.h>
-#include <linux/v4l2-controls.h>
 
 #include <libcamera/buffer.h>
 #include <libcamera/control_ids.h>
@@ -25,6 +24,7 @@
 #include <libipa/ipa_interface_wrapper.h>
 
 #include "libcamera/internal/log.h"
+#include "libcamera/internal/v4l2_controls.h"
 
 namespace libcamera {
 
@@ -91,13 +91,13 @@ void IPARkISP1::configure([[maybe_unused]] const CameraSensorInfo &info,
 
 	ctrls_ = entityControls.at(0);
 
-	const auto itExp = ctrls_.find(V4L2_CID_EXPOSURE);
+	const auto itExp = ctrls_.find(&v4l2::EXPOSURE);
 	if (itExp == ctrls_.end()) {
 		LOG(IPARkISP1, Error) << "Can't find exposure control";
 		return;
 	}
 
-	const auto itGain = ctrls_.find(V4L2_CID_ANALOGUE_GAIN);
+	const auto itGain = ctrls_.find(&v4l2::ANALOGUE_GAIN);
 	if (itGain == ctrls_.end()) {
 		LOG(IPARkISP1, Error) << "Can't find gain control";
 		return;
@@ -261,8 +261,8 @@ void IPARkISP1::setControls(unsigned int frame)
 	op.operation = RKISP1_IPA_ACTION_V4L2_SET;
 
 	ControlList ctrls(ctrls_);
-	ctrls.set(V4L2_CID_EXPOSURE, static_cast<int32_t>(exposure_));
-	ctrls.set(V4L2_CID_ANALOGUE_GAIN, static_cast<int32_t>(gain_));
+	ctrls.set(v4l2::EXPOSURE, exposure_);
+	ctrls.set(v4l2::ANALOGUE_GAIN, gain_);
 	op.controls.push_back(ctrls);
 
 	queueFrameAction.emit(frame, op);
diff --git a/src/libcamera/camera_sensor.cpp b/src/libcamera/camera_sensor.cpp
index c9e8d49b7935..eb96c4841708 100644
--- a/src/libcamera/camera_sensor.cpp
+++ b/src/libcamera/camera_sensor.cpp
@@ -280,16 +280,16 @@ int CameraSensor::validateSensorDriver()
 	 * Optional controls are used to register optional sensor properties. If
 	 * not present, some values will be defaulted.
 	 */
-	static constexpr uint32_t optionalControls[] = {
-		V4L2_CID_CAMERA_ORIENTATION,
-		V4L2_CID_CAMERA_SENSOR_ROTATION,
+	static const ControlId *const optionalControls[] = {
+		&v4l2::CAMERA_ORIENTATION,
+		&v4l2::CAMERA_SENSOR_ROTATION,
 	};
 
-	const ControlIdMap &controls = subdev_->controls().idmap();
-	for (uint32_t ctrl : optionalControls) {
+	const ControlInfoMap &controls = subdev_->controls();
+	for (const ControlId *ctrl : optionalControls) {
 		if (!controls.count(ctrl))
 			LOG(CameraSensor, Debug)
-				<< "Optional V4L2 control " << utils::hex(ctrl)
+				<< "Optional V4L2 control " << ctrl->name()
 				<< " not supported";
 	}
 
@@ -346,18 +346,18 @@ int CameraSensor::validateSensorDriver()
 	 * For raw sensors, make sure the sensor driver supports the controls
 	 * required by the CameraSensor class.
 	 */
-	static constexpr uint32_t mandatoryControls[] = {
-		V4L2_CID_EXPOSURE,
-		V4L2_CID_HBLANK,
-		V4L2_CID_PIXEL_RATE,
-		V4L2_CID_VBLANK,
+	static const ControlId *const mandatoryControls[] = {
+		&v4l2::EXPOSURE,
+		&v4l2::HBLANK,
+		&v4l2::PIXEL_RATE,
+		&v4l2::VBLANK,
 	};
 
 	err = 0;
-	for (uint32_t ctrl : mandatoryControls) {
+	for (const ControlId *ctrl : mandatoryControls) {
 		if (!controls.count(ctrl)) {
 			LOG(CameraSensor, Error)
-				<< "Mandatory V4L2 control " << utils::hex(ctrl)
+				<< "Mandatory V4L2 control " << ctrl->name()
 				<< " not available";
 			err = -EINVAL;
 		}
@@ -425,7 +425,7 @@ int CameraSensor::initProperties()
 	const ControlInfoMap &controls = subdev_->controls();
 	int32_t propertyValue;
 
-	const auto &orientation = controls.find(V4L2_CID_CAMERA_ORIENTATION);
+	const auto &orientation = controls.find(&v4l2::CAMERA_ORIENTATION);
 	if (orientation != controls.end()) {
 		int32_t v4l2Orientation = orientation->second.def().get<int32_t>();
 
@@ -450,7 +450,7 @@ int CameraSensor::initProperties()
 	}
 	properties_.set(properties::Location, propertyValue);
 
-	const auto &rotationControl = controls.find(V4L2_CID_CAMERA_SENSOR_ROTATION);
+	const auto &rotationControl = controls.find(&v4l2::CAMERA_SENSOR_ROTATION);
 	if (rotationControl != controls.end()) {
 		propertyValue = rotationControl->second.def().get<int32_t>();
 		properties_.set(properties::Rotation, propertyValue);
@@ -775,11 +775,11 @@ int CameraSensor::sensorInfo(CameraSensorInfo *info) const
 		return -EINVAL;
 	}
 
-	int32_t hblank = ctrls.get(V4L2_CID_HBLANK).get<int32_t>();
+	int32_t hblank = ctrls.get(v4l2::HBLANK);
 	info->lineLength = info->outputSize.width + hblank;
-	info->pixelRate = ctrls.get(V4L2_CID_PIXEL_RATE).get<int64_t>();
+	info->pixelRate = ctrls.get(v4l2::PIXEL_RATE);
 
-	const ControlInfo vblank = ctrls.infoMap()->at(V4L2_CID_VBLANK);
+	const ControlInfo vblank = ctrls.infoMap()->at(&v4l2::VBLANK);
 	info->minFrameLength = info->outputSize.height + vblank.min().get<int32_t>();
 	info->maxFrameLength = info->outputSize.height + vblank.max().get<int32_t>();
 
diff --git a/src/libcamera/pipeline/ipu3/ipu3.cpp b/src/libcamera/pipeline/ipu3/ipu3.cpp
index 9bc3df3352fd..d3fc51518598 100644
--- a/src/libcamera/pipeline/ipu3/ipu3.cpp
+++ b/src/libcamera/pipeline/ipu3/ipu3.cpp
@@ -845,7 +845,7 @@ int PipelineHandlerIPU3::initControls(IPU3CameraData *data)
 	double lineDuration = sensorInfo.lineLength
 			    / (sensorInfo.pixelRate / 1e6);
 	const ControlInfoMap &sensorControls = sensor->controls();
-	const ControlInfo &v4l2Exposure = sensorControls.find(V4L2_CID_EXPOSURE)->second;
+	const ControlInfo &v4l2Exposure = sensorControls.find(&v4l2::EXPOSURE)->second;
 	int32_t minExposure = v4l2Exposure.min().get<int32_t>() * lineDuration;
 	int32_t maxExposure = v4l2Exposure.max().get<int32_t>() * lineDuration;
 	int32_t defExposure = v4l2Exposure.def().get<int32_t>() * lineDuration;
diff --git a/src/libcamera/pipeline/raspberrypi/raspberrypi.cpp b/src/libcamera/pipeline/raspberrypi/raspberrypi.cpp
index 0804a4393c19..90f846530346 100644
--- a/src/libcamera/pipeline/raspberrypi/raspberrypi.cpp
+++ b/src/libcamera/pipeline/raspberrypi/raspberrypi.cpp
@@ -992,8 +992,8 @@ bool PipelineHandlerRPi::match(DeviceEnumerator *enumerator)
 		data->flipsAlterBayerOrder_ = hflipCtrl->flags & V4L2_CTRL_FLAG_MODIFY_LAYOUT;
 
 		ControlList ctrls(dev->controls());
-		ctrls.set(V4L2_CID_HFLIP, 0);
-		ctrls.set(V4L2_CID_VFLIP, 0);
+		ctrls.set(v4l2::HFLIP, 0);
+		ctrls.set(v4l2::VFLIP, 0);
 		dev->setControls(&ctrls);
 	}
 
@@ -1246,10 +1246,10 @@ int RPiCameraData::configureIPA(const CameraConfiguration *config)
 	 */
 	if (supportsFlips_) {
 		ControlList ctrls(unicam_[Unicam::Image].dev()->controls());
-		ctrls.set(V4L2_CID_HFLIP,
-			  static_cast<int32_t>(!!(rpiConfig->combinedTransform_ & Transform::HFlip)));
-		ctrls.set(V4L2_CID_VFLIP,
-			  static_cast<int32_t>(!!(rpiConfig->combinedTransform_ & Transform::VFlip)));
+		ctrls.set(v4l2::HFLIP,
+			  !!(rpiConfig->combinedTransform_ & Transform::HFlip));
+		ctrls.set(v4l2::VFLIP,
+			  !!(rpiConfig->combinedTransform_ & Transform::VFlip));
 		unicam_[Unicam::Image].dev()->setControls(&ctrls);
 	}
 
@@ -1383,8 +1383,8 @@ void RPiCameraData::unicamBufferDequeue(FrameBuffer *buffer)
 			auto it = mappedEmbeddedBuffers_.find(bufferId);
 			if (it != mappedEmbeddedBuffers_.end()) {
 				uint32_t *mem = reinterpret_cast<uint32_t *>(it->second.maps()[0].data());
-				mem[0] = ctrl.get(V4L2_CID_EXPOSURE).get<int32_t>();
-				mem[1] = ctrl.get(V4L2_CID_ANALOGUE_GAIN).get<int32_t>();
+				mem[0] = ctrl.get(v4l2::EXPOSURE);
+				mem[1] = ctrl.get(v4l2::ANALOGUE_GAIN);
 			} else {
 				LOG(RPI, Warning) << "Failed to find embedded buffer "
 						  << bufferId;
diff --git a/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp b/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp
index 08a594175091..a80b51f174a9 100644
--- a/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp
+++ b/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp
@@ -260,20 +260,20 @@ void PipelineHandlerUVC::stop(Camera *camera)
 int PipelineHandlerUVC::processControl(ControlList *controls, unsigned int id,
 				       const ControlValue &value)
 {
-	uint32_t cid;
+	const ControlId *cid;
 
 	if (id == controls::Brightness)
-		cid = V4L2_CID_BRIGHTNESS;
+		cid = &v4l2::BRIGHTNESS;
 	else if (id == controls::Contrast)
-		cid = V4L2_CID_CONTRAST;
+		cid = &v4l2::CONTRAST;
 	else if (id == controls::Saturation)
-		cid = V4L2_CID_SATURATION;
+		cid = &v4l2::SATURATION;
 	else if (id == controls::AeEnable)
-		cid = V4L2_CID_EXPOSURE_AUTO;
+		cid = &v4l2::EXPOSURE_AUTO;
 	else if (id == controls::ExposureTime)
-		cid = V4L2_CID_EXPOSURE_ABSOLUTE;
+		cid = &v4l2::EXPOSURE_ABSOLUTE;
 	else if (id == controls::AnalogueGain)
-		cid = V4L2_CID_GAIN;
+		cid = &v4l2::GAIN;
 	else
 		return -EINVAL;
 
@@ -286,18 +286,18 @@ int PipelineHandlerUVC::processControl(ControlList *controls, unsigned int id,
 	 * See UVCCameraData::addControl() for explanations of the different
 	 * value mappings.
 	 */
-	switch (cid) {
+	switch (cid->id()) {
 	case V4L2_CID_BRIGHTNESS: {
 		float scale = std::max(max - def, def - min);
 		float fvalue = value.get<float>() * scale + def;
-		controls->set(cid, static_cast<int32_t>(lroundf(fvalue)));
+		controls->set(v4l2::BRIGHTNESS, lroundf(fvalue));
 		break;
 	}
 
 	case V4L2_CID_SATURATION: {
 		float scale = def - min;
 		float fvalue = value.get<float>() * scale + min;
-		controls->set(cid, static_cast<int32_t>(lroundf(fvalue)));
+		controls->set(v4l2::SATURATION, lroundf(fvalue));
 		break;
 	}
 
@@ -305,12 +305,13 @@ int PipelineHandlerUVC::processControl(ControlList *controls, unsigned int id,
 		int32_t ivalue = value.get<bool>()
 			       ? V4L2_EXPOSURE_APERTURE_PRIORITY
 			       : V4L2_EXPOSURE_MANUAL;
-		controls->set(V4L2_CID_EXPOSURE_AUTO, ivalue);
+		controls->set(v4l2::EXPOSURE_AUTO, ivalue);
 		break;
 	}
 
 	case V4L2_CID_EXPOSURE_ABSOLUTE:
-		controls->set(cid, value.get<int32_t>() / 100);
+		controls->set(v4l2::EXPOSURE_ABSOLUTE,
+			      value.get<int32_t>() / 100);
 		break;
 
 	case V4L2_CID_CONTRAST:
@@ -324,13 +325,13 @@ int PipelineHandlerUVC::processControl(ControlList *controls, unsigned int id,
 		}
 
 		float fvalue = (value.get<float>() - p) / m;
-		controls->set(cid, static_cast<int32_t>(lroundf(fvalue)));
+		controls->set(cid->id(), static_cast<int32_t>(lroundf(fvalue)));
 		break;
 	}
 
 	default: {
 		int32_t ivalue = value.get<int32_t>();
-		controls->set(cid, ivalue);
+		controls->set(cid->id(), ivalue);
 		break;
 	}
 	}
diff --git a/src/libcamera/pipeline/vimc/vimc.cpp b/src/libcamera/pipeline/vimc/vimc.cpp
index 36325ffbbd7d..24b39b7dd879 100644
--- a/src/libcamera/pipeline/vimc/vimc.cpp
+++ b/src/libcamera/pipeline/vimc/vimc.cpp
@@ -342,24 +342,24 @@ int PipelineHandlerVimc::processControls(VimcCameraData *data, Request *request)
 
 	for (auto it : request->controls()) {
 		unsigned int id = it.first;
+		const ControlId *cid;
 		unsigned int offset;
-		uint32_t cid;
 
 		if (id == controls::Brightness) {
-			cid = V4L2_CID_BRIGHTNESS;
+			cid = &v4l2::BRIGHTNESS;
 			offset = 128;
 		} else if (id == controls::Contrast) {
-			cid = V4L2_CID_CONTRAST;
+			cid = &v4l2::CONTRAST;
 			offset = 0;
 		} else if (id == controls::Saturation) {
-			cid = V4L2_CID_SATURATION;
+			cid = &v4l2::SATURATION;
 			offset = 0;
 		} else {
 			continue;
 		}
 
 		int32_t value = lroundf(it.second.get<float>() * 128 + offset);
-		controls.set(cid, std::clamp(value, 0, 255));
+		controls.set(cid->id(), std::clamp(value, 0, 255));
 	}
 
 	for (const auto &ctrl : controls)
