diff --git a/include/libcamera/internal/control_serializer.h b/include/libcamera/internal/control_serializer.h
index 8a63ae44a13e..307ecba572fc 100644
--- a/include/libcamera/internal/control_serializer.h
+++ b/include/libcamera/internal/control_serializer.h
@@ -47,9 +47,13 @@ private:
 	static void store(const ControlValue &value, ByteStreamBuffer &buffer);
 	static void store(const ControlInfo &info, ByteStreamBuffer &buffer);
 
+	void populateControlValueEntry(struct ipa_control_value_entry &entry,
+				       const ControlValue &value,
+				       uint32_t offset);
+
 	ControlValue loadControlValue(ByteStreamBuffer &buffer,
-				      bool isArray = false, unsigned int count = 1);
-	ControlInfo loadControlInfo(ByteStreamBuffer &buffer);
+				      ControlType type,
+				      bool isArray, unsigned int count);
 
 	unsigned int serial_;
 	unsigned int serialSeed_;
diff --git a/include/libcamera/ipa/ipa_controls.h b/include/libcamera/ipa/ipa_controls.h
index 980668c86bcc..6af962ff325e 100644
--- a/include/libcamera/ipa/ipa_controls.h
+++ b/include/libcamera/ipa/ipa_controls.h
@@ -15,7 +15,7 @@ namespace libcamera {
 extern "C" {
 #endif
 
-#define IPA_CONTROLS_FORMAT_VERSION	1
+#define IPA_CONTROLS_FORMAT_VERSION	2
 
 enum ipa_controls_id_map_type {
 	IPA_CONTROL_ID_MAP_CONTROLS,
@@ -34,20 +34,26 @@ struct ipa_controls_header {
 };
 
 struct ipa_control_value_entry {
-	uint32_t id;
 	uint8_t type;
 	uint8_t is_array;
 	uint16_t count;
 	uint32_t offset;
-	uint32_t padding[1];
+	uint32_t reserved[2];
+};
+
+struct ipa_control_list_entry {
+	uint32_t id;
+	struct ipa_control_value_entry value;
 };
 
 struct ipa_control_info_entry {
 	uint32_t id;
 	uint32_t type;
-	uint32_t offset;
 	uint8_t direction;
-	uint8_t padding[3];
+	uint8_t padding[7];
+	struct ipa_control_value_entry min;
+	struct ipa_control_value_entry max;
+	struct ipa_control_value_entry def;
 };
 
 #ifdef __cplusplus
diff --git a/src/libcamera/control_serializer.cpp b/src/libcamera/control_serializer.cpp
index 050f8512bd52..843e2772f848 100644
--- a/src/libcamera/control_serializer.cpp
+++ b/src/libcamera/control_serializer.cpp
@@ -144,7 +144,7 @@ void ControlSerializer::reset()
 
 size_t ControlSerializer::binarySize(const ControlValue &value)
 {
-	return sizeof(ControlType) + value.data().size_bytes();
+	return value.data().size_bytes();
 }
 
 size_t ControlSerializer::binarySize(const ControlInfo &info)
@@ -164,7 +164,8 @@ size_t ControlSerializer::binarySize(const ControlInfo &info)
 size_t ControlSerializer::binarySize(const ControlInfoMap &infoMap)
 {
 	size_t size = sizeof(struct ipa_controls_header)
-		    + infoMap.size() * sizeof(struct ipa_control_info_entry);
+		    + infoMap.size() * (sizeof(struct ipa_control_info_entry) +
+					3 * sizeof(struct ipa_control_value_entry));
 
 	for (const auto &ctrl : infoMap)
 		size += binarySize(ctrl.second);
@@ -184,7 +185,7 @@ size_t ControlSerializer::binarySize(const ControlInfoMap &infoMap)
 size_t ControlSerializer::binarySize(const ControlList &list)
 {
 	size_t size = sizeof(struct ipa_controls_header)
-		    + list.size() * sizeof(struct ipa_control_value_entry);
+		    + list.size() * sizeof(struct ipa_control_list_entry);
 
 	for (const auto &ctrl : list)
 		size += binarySize(ctrl.second);
@@ -195,16 +196,17 @@ size_t ControlSerializer::binarySize(const ControlList &list)
 void ControlSerializer::store(const ControlValue &value,
 			      ByteStreamBuffer &buffer)
 {
-	const ControlType type = value.type();
-	buffer.write(&type);
 	buffer.write(value.data());
 }
 
-void ControlSerializer::store(const ControlInfo &info, ByteStreamBuffer &buffer)
+void ControlSerializer::populateControlValueEntry(struct ipa_control_value_entry &entry,
+						  const ControlValue &value,
+						  uint32_t offset)
 {
-	store(info.min(), buffer);
-	store(info.max(), buffer);
-	store(info.def(), buffer);
+	entry.type = value.type();
+	entry.is_array = value.isArray();
+	entry.count = value.numElements();
+	entry.offset = offset;
 }
 
 /**
@@ -232,7 +234,8 @@ int ControlSerializer::serialize(const ControlInfoMap &infoMap,
 
 	/* Compute entries and data required sizes. */
 	size_t entriesSize = infoMap.size()
-			   * sizeof(struct ipa_control_info_entry);
+			   * (sizeof(struct ipa_control_info_entry) +
+			      3 * sizeof(struct ipa_control_value_entry));
 	size_t valuesSize = 0;
 	for (const auto &ctrl : infoMap)
 		valuesSize += binarySize(ctrl.second);
@@ -280,11 +283,18 @@ int ControlSerializer::serialize(const ControlInfoMap &infoMap,
 		struct ipa_control_info_entry entry;
 		entry.id = id->id();
 		entry.type = id->type();
-		entry.offset = values.offset();
 		entry.direction = static_cast<ControlId::DirectionFlags::Type>(id->direction());
-		entries.write(&entry);
 
-		store(info, values);
+		populateControlValueEntry(entry.min, info.min(), values.offset());
+		store(info.min(), values);
+
+		populateControlValueEntry(entry.max, info.max(), values.offset());
+		store(info.max(), values);
+
+		populateControlValueEntry(entry.def, info.def(), values.offset());
+		store(info.def(), values);
+
+		entries.write(&entry);
 	}
 
 	if (buffer.overflow())
@@ -341,7 +351,7 @@ int ControlSerializer::serialize(const ControlList &list,
 	else
 		idMapType = IPA_CONTROL_ID_MAP_V4L2;
 
-	size_t entriesSize = list.size() * sizeof(struct ipa_control_value_entry);
+	size_t entriesSize = list.size() * sizeof(struct ipa_control_list_entry);
 	size_t valuesSize = 0;
 	for (const auto &ctrl : list)
 		valuesSize += binarySize(ctrl.second);
@@ -365,12 +375,9 @@ int ControlSerializer::serialize(const ControlList &list,
 		unsigned int id = ctrl.first;
 		const ControlValue &value = ctrl.second;
 
-		struct ipa_control_value_entry entry;
+		struct ipa_control_list_entry entry;
 		entry.id = id;
-		entry.type = value.type();
-		entry.is_array = value.isArray();
-		entry.count = value.numElements();
-		entry.offset = values.offset();
+		populateControlValueEntry(entry.value, value, values.offset());
 		entries.write(&entry);
 
 		store(value, values);
@@ -383,12 +390,10 @@ int ControlSerializer::serialize(const ControlList &list,
 }
 
 ControlValue ControlSerializer::loadControlValue(ByteStreamBuffer &buffer,
+						 ControlType type,
 						 bool isArray,
 						 unsigned int count)
 {
-	ControlType type;
-	buffer.read(&type);
-
 	ControlValue value;
 
 	value.reserve(type, isArray, count);
@@ -397,15 +402,6 @@ ControlValue ControlSerializer::loadControlValue(ByteStreamBuffer &buffer,
 	return value;
 }
 
-ControlInfo ControlSerializer::loadControlInfo(ByteStreamBuffer &b)
-{
-	ControlValue min = loadControlValue(b);
-	ControlValue max = loadControlValue(b);
-	ControlValue def = loadControlValue(b);
-
-	return ControlInfo(min, max, def);
-}
-
 /**
  * \fn template<typename T> T ControlSerializer::deserialize(ByteStreamBuffer &buffer)
  * \brief Deserialize an object from a binary buffer
@@ -483,8 +479,7 @@ ControlInfoMap ControlSerializer::deserialize<ControlInfoMap>(ByteStreamBuffer &
 
 	ControlInfoMap::Map ctrls;
 	for (unsigned int i = 0; i < hdr->entries; ++i) {
-		const struct ipa_control_info_entry *entry =
-			entries.read<decltype(*entry)>();
+		const auto *entry = entries.read<const ipa_control_info_entry>();
 		if (!entry) {
 			LOG(Serializer, Error) << "Out of data";
 			return {};
@@ -511,15 +506,43 @@ ControlInfoMap ControlSerializer::deserialize<ControlInfoMap>(ByteStreamBuffer &
 		const ControlId *controlId = idMap->at(entry->id);
 		ASSERT(controlId);
 
-		if (entry->offset != values.offset()) {
+		const ipa_control_value_entry &min_entry = entry->min;
+		const ipa_control_value_entry &max_entry = entry->max;
+		const ipa_control_value_entry &def_entry = entry->def;
+
+		if (min_entry.offset != values.offset()) {
 			LOG(Serializer, Error)
-				<< "Bad data, entry offset mismatch (entry "
+				<< "Bad data, entry offset mismatch (min entry "
 				<< i << ")";
 			return {};
 		}
+		ControlValue min =
+			loadControlValue(values, static_cast<ControlType>(min_entry.type),
+					 min_entry.is_array, min_entry.count);
+
+		if (max_entry.offset != values.offset()) {
+			LOG(Serializer, Error)
+				<< "Bad data, entry offset mismatch (max entry "
+				<< i << ")";
+			return {};
+		}
+		ControlValue max =
+			loadControlValue(values, static_cast<ControlType>(max_entry.type),
+					 max_entry.is_array, max_entry.count);
+
+		if (def_entry.offset != values.offset()) {
+			LOG(Serializer, Error)
+				<< "Bad data, entry offset mismatch (def entry "
+				<< i << ")";
+			return {};
+		}
+		ControlValue def =
+			loadControlValue(values, static_cast<ControlType>(def_entry.type),
+					 def_entry.is_array, def_entry.count);
+
 
 		/* Create and store the ControlInfo. */
-		ctrls.emplace(controlId, loadControlInfo(values));
+		ctrls.emplace(controlId, ControlInfo(min, max, def));
 	}
 
 	/*
@@ -618,12 +641,12 @@ ControlList ControlSerializer::deserialize<ControlList>(ByteStreamBuffer &buffer
 	ControlList ctrls(*idMap);
 
 	for (unsigned int i = 0; i < hdr->entries; ++i) {
-		const struct ipa_control_value_entry *entry =
-			entries.read<decltype(*entry)>();
-		if (!entry) {
+		auto *list_entry = entries.read<const ipa_control_list_entry>();
+		if (!list_entry) {
 			LOG(Serializer, Error) << "Out of data";
 			return {};
 		}
+		const ipa_control_value_entry *entry = &list_entry->value;
 
 		if (entry->offset != values.offset()) {
 			LOG(Serializer, Error)
@@ -632,8 +655,9 @@ ControlList ControlSerializer::deserialize<ControlList>(ByteStreamBuffer &buffer
 			return {};
 		}
 
-		ctrls.set(entry->id,
-			  loadControlValue(values, entry->is_array, entry->count));
+		ctrls.set(list_entry->id,
+			  loadControlValue(values, static_cast<ControlType>(entry->type),
+					   entry->is_array, entry->count));
 	}
 
 	return ctrls;
diff --git a/src/libcamera/ipa_controls.cpp b/src/libcamera/ipa_controls.cpp
index 12d92ebe894d..61af7433da17 100644
--- a/src/libcamera/ipa_controls.cpp
+++ b/src/libcamera/ipa_controls.cpp
@@ -26,28 +26,28 @@
  * The following diagram describes the layout of the ControlList packet.
  *
  * ~~~~
- *           +-------------------------+    .                      .
- *  Header / | ipa_controls_header     |    |                      |
- *         | |                         |    |                      |
- *         \ |                         |    |                      |
- *           +-------------------------+    |                      |
- *         / | ipa_control_value_entry |    | hdr.data_offset      |
- *         | | #0                      |    |                      |
- * Control | +-------------------------+    |                      |
- *   value | | ...                     |    |                      |
- * entries | +-------------------------+    |                      |
- *         | | ipa_control_value_entry |    |             hdr.size |
- *         \ | #hdr.entries - 1        |    |                      |
- *           +-------------------------+    |                      |
- *           | empty space (optional)  |    |                      |
- *           +-------------------------+ <--´  .                   |
- *         / | ...                     |       | entry[n].offset   |
- *    Data | | ...                     |       |                   |
- * section | | value data for entry #n | <-----´                   |
- *         \ | ...                     |                           |
- *           +-------------------------+                           |
- *           | empty space (optional)  |                           |
- *           +-------------------------+ <-------------------------´
+ *           +-------------------------+    .                          .
+ *  Header / | ipa_controls_header     |    |                          |
+ *         | |                         |    |                          |
+ *         \ |                         |    |                          |
+ *           +-------------------------+    |                          |
+ *         / | ipa_control_list_entry  |    | hdr.data_offset          |
+ *         | | #0                      |    |                          |
+ * Control | +-------------------------+    |                          |
+ *   value | | ...                     |    |                          |
+ * entries | +-------------------------+    |                          |
+ *         | | ipa_control_list_entry  |    |             hdr.size     |
+ *         \ | #hdr.entries - 1        |    |                          |
+ *           +-------------------------+    |                          |
+ *           | empty space (optional)  |    |                          |
+ *           +-------------------------+ <--´  .                       |
+ *         / | ...                     |       | entry[n].value.offset |
+ *    Data | | ...                     |       |                       |
+ * section | | value data for entry #n | <-----´                       |
+ *         \ | ...                     |                               |
+ *           +-------------------------+                               |
+ *           | empty space (optional)  |                               |
+ *           +-------------------------+ <-----------------------------´
  * ~~~~
  *
  * The packet header contains the size of the packet, the number of entries, and
@@ -56,12 +56,14 @@
  * offset ipa_controls_header::data_offset from the beginning of the packet, and
  * shall be aligned to a multiple of 8 bytes.
  *
- * Entries are described by the ipa_control_value_entry structure. They contain
- * the numerical ID of the control, its type, and the number of control values.
+ * Entries are described by the ipa_control_list_entry structure. They contain
+ * the numerical ID of the control and an ipa_control_value_entry structure,
+ * which contains the type and the number of control values.
  *
- * The control values are stored in the data section in the platform's native
- * format. The ipa_control_value_entry::offset field stores the offset from the
- * beginning of the data section to the values.
+ * The control values are stored (as ipa_control_list_entry) in the data
+ * section in the platform's native format. The ipa_control_value_entry::offset
+ * field stores the offset from the beginning of the data section to the
+ * values.
  *
  * All control values in the data section shall be stored in the same order as
  * the respective control entries, shall be aligned to a multiple of 8 bytes,
@@ -74,59 +76,65 @@
  * The following diagram describes the layout of the ControlInfoMap packet.
  *
  * ~~~~
- *           +-------------------------+    .                      .
- *  Header / | ipa_controls_header     |    |                      |
- *         | |                         |    |                      |
- *         \ |                         |    |                      |
- *           +-------------------------+    |                      |
- *         / | ipa_control_info_entry  |    | hdr.data_offset      |
- *         | | #0                      |    |                      |
- * Control | +-------------------------+    |                      |
- *    info | | ...                     |    |                      |
- * entries | +-------------------------+    |                      |
- *         | | ipa_control_info_entry  |    |             hdr.size |
- *         \ | #hdr.entries - 1        |    |                      |
- *           +-------------------------+    |                      |
- *           | empty space (optional)  |    |                      |
- *           +-------------------------+ <--´  .                   |
- *         / | ...                     |       | entry[n].offset   |
- *    Data | | ...                     |       |                   |
- * section | | info data for entry #n  | <-----´                   |
- *         \ | ...                     |                           |
- *           +-------------------------+                           |
- *           | empty space (optional)  |                           |
- *           +-------------------------+ <-------------------------´
+ *           +------------------------------+    .                            .
+ *  Header / | ipa_controls_header          |    |                            |
+ *         | |                              |    |                            |
+ *         \ |                              |    |                            |
+ *           +------------------------------+    |                            |
+ *         / | ipa_control_info_entry       |    | hdr.data_offset            |
+ *         | | #0                           |    |                            |
+ * Control | +------------------------------+    |                            |
+ *    info | | ...                          |    |                            |
+ * entries | +------------------------------+    |                            |
+ *         | | ipa_control_info_entry       |    |                            |
+ *         \ | #hdr.entries - 1             |    |                            |
+ *           +------------------------------+    |                            |
+ *           | empty space (optional)       |    |                            |
+ *           +------------------------------+ <--´  . . .                     |
+ *         / | ...                          |       | entry[n].min.offset     |
+ *         | | ...                          |       | | |                     |
+ *    Data | | ...                          |       | | entry[n].max.offset   |
+ * section | | min value data for entry #n  | <-----´ | |                     |
+ *         | | max value data for entry #n  | <-------´ | entry[n].def.offset |
+ *         | | def value data for entry #n  | <---------´                     |
+ *         | | ...                          |                                 |
+ *         \ | ...                          |                                 |
+ *           +------------------------------+                                 |
+ *           | empty space (optional)       |                                 |
+ *           +------------------------------+ <-------------------------------´
  * ~~~~
  *
  * The packet header is identical to the ControlList packet header.
  *
  * Entries are described by the ipa_control_info_entry structure. They contain
- * the numerical ID and type of the control. The control info data is stored
- * in the data section as described by the following diagram.
+ * the numerical ID, direction (in/out) of the control, and three
+ * ipa_control_value_entry structures for the min, max, and def ControlValues
+ * that make up the ControlInfo.
  *
- * ~~~~
- *           +-------------------------+       .
- *         / | ...                     |       | entry[n].offset
- *         | +-------------------------+ <-----´
- *         | | minimum value (#n)      | \
- *    Data | +-------------------------+ |
- * section | | maximum value (#n)      | | Entry #n
- *         | +-------------------------+ |
- *         | | default value (#n)      | /
- *         | +-------------------------+
- *         \ | ...                     |
- *           +-------------------------+
- * ~~~~
+ * The control info has no associated data in the data section;
+ * instead the three control values for min, max, and def are stored in the data section
+ *
+ *
+ * The control values are stored (as ipa_control_list_entry) in the data
+ * section in the platform's native format. The ipa_control_value_entry::offset
+ * field stores the offset from the beginning of the data section to the
+ * values.
  *
- * The minimum, maximum and default values are stored in the platform's native
- * data format. The ipa_control_info_entry::offset field stores the offset from
- * the beginning of the data section to the info data.
+ * ipa_control_value_entry structures contain the relevant
+ * ControlValue information for the ControlInfo's min, max, and def
+ * respectively, and their associated data is stored in the data section.
  *
- * Info data in the data section shall be stored in the same order as the
- * entries array, shall be aligned to a multiple of 8 bytes, and shall be
- * contiguous in memory.
+ * The control info has no associated data in the data section. Instead the
+ * minimum, maximum, and default control values of the control info are stored
+ * n the data section in the platform's native data format. The
+ * ipa_control_value_entry::offset field stores the offset from the beginning
+ * of the data section to the control value data.
  *
- * As for the ControlList packet, empty spaces may be present between the end of
+ * All control values in the data section shall be stored in the same order as
+ * the respective control info entries, in the order of min, max, def, and shall be
+ * aligned to a multiple of 8 bytes, and shall be contiguous in memory.
+ *
+ * As with the ControlList packet, empty spaces may be present between the end of
  * the entries array and the data section, and after the data section. They
  * shall be ignored when parsing the packet.
  */
@@ -192,8 +200,6 @@ static_assert(sizeof(ipa_controls_header) == 32,
 /**
  * \struct ipa_control_value_entry
  * \brief Description of a serialized ControlValue entry
- * \var ipa_control_value_entry::id
- * The numerical ID of the control
  * \var ipa_control_value_entry::type
  * The type of the control (defined by enum ControlType)
  * \var ipa_control_value_entry::is_array
@@ -203,13 +209,25 @@ static_assert(sizeof(ipa_controls_header) == 32,
  * \var ipa_control_value_entry::offset
  * The offset in bytes from the beginning of the data section to the control
  * value data (shall be a multiple of 8 bytes).
- * \var ipa_control_value_entry::padding
- * Padding bytes (shall be set to 0)
+ * \var ipa_control_value_entry::reserved
+ * Reserved for future extensions
  */
 
 static_assert(sizeof(ipa_control_value_entry) == 16,
 	      "Invalid ABI size change for struct ipa_control_value_entry");
 
+/**
+ * \struct ipa_control_list_entry
+ * \brief Description of a serialized ControlList entry
+ * \var ipa_control_list_entry::id
+ * The numerical ID of the control
+ * \var ipa_control_list_entry::value
+ * The description of the serialized ControlValue
+ */
+
+static_assert(sizeof(ipa_control_list_entry) == 20,
+	      "Invalid ABI size change for struct ipa_control_list_entry");
+
 /**
  * \struct ipa_control_info_entry
  * \brief Description of a serialized ControlInfo entry
@@ -217,8 +235,6 @@ static_assert(sizeof(ipa_control_value_entry) == 16,
  * The numerical ID of the control
  * \var ipa_control_info_entry::type
  * The type of the control (defined by enum ControlType)
- * \var ipa_control_info_entry::offset
- * The offset in bytes from the beginning of the data section to the control
  * info data (shall be a multiple of 8 bytes)
  * \var ipa_control_info_entry::direction
  * The directions in which the control is allowed to be sent. This is a flags
@@ -226,9 +242,15 @@ static_assert(sizeof(ipa_control_value_entry) == 16,
  * metadata). \sa ControlId::Direction
  * \var ipa_control_info_entry::padding
  * Padding bytes (shall be set to 0)
+ * \var ipa_control_info_entry::min
+ * The description of the serialized ControlValue (min)
+ * \var ipa_control_info_entry::max
+ * The description of the serialized ControlValue (max)
+ * \var ipa_control_info_entry::def
+ * The description of the serialized ControlValue (def)
  */
 
-static_assert(sizeof(ipa_control_info_entry) == 16,
+static_assert(sizeof(ipa_control_info_entry) == 64,
 	      "Invalid ABI size change for struct ipa_control_info_entry");
 
 } /* namespace libcamera */
