Изучаем программирование встраиваемых систем на C: Понятие объекта данных union (объединение)

Добавлено 10 июня 2019 в 06:48

Рассмотрим объекты данных, называемые union (объединение) и используемые при программировании для встраиваемых систем на языке C.

Различие между структурой и объединением в программировании на C для встраиваемых систем

В предыдущей статье этой серии мы обсуждали, что структуры в языке C позволяют группировать переменные разных типов и работать с ними как с одним объектом данных.

В дополнение к структурам язык C поддерживает другую конструкцию данных, называемую объединением (union), которая может группировать различные типы данных в один объект данных. Данная статья предоставляет основную информацию об объединениях. Сначала мы рассмотрим вводный пример объявления объединения, а затем рассмотрим важное применение этого объекта данных.

Вводный пример

Объявление объединения очень похоже на объявление структуры. Нам нужно только заменить ключевое слово "struct" на "union". Рассмотрим следующий пример кода:

union test {
  uint8_t   c;
  uint32_t  i;
};

Данный код определяет шаблон, который имеет два члена: "c", который занимает один байт, и "i", который занимает четыре байта.

Теперь мы можем создать переменную этого шаблона объединения:

union test u1;

Получить доступ к членам объединения "u1" мы можем, используя оператор члена (.). Например, следующий код присваивает значение 10 второму члену приведенного выше объединения и копирует значение "c" в переменную "m" (которая должна иметь тип uint8_t).

u1.i=10;
m=u1.c;

Какой объем памяти будет выделен для хранения переменной "u1"? Принимая во внимание, что размер структуры, по меньшей мере, равен сумме размеров ее членов, размер объединения равен размеру его наибольшей переменной. Пространство памяти, выделенное объединению, будет совместно использоваться всеми членами объединения. В приведенном выше примере размер "u1" равен размеру uint32_t, то есть четыре байта. Это пространство памяти распределяется между "i" и "c". Следовательно, присвоение значения одному из этих двух членов, изменит значение другого члена.

Вы можете задаться вопросом: «Какой смысл использовать одно и то же пространство памяти для хранения нескольких переменных? Есть ли какое-либо применение для этой особенности?». Мы рассмотрим этот вопрос в следующем разделе.

Нужно ли нам общее пространство памяти?

Давайте рассмотрим пример, где объединение может быть полезным объектом данных. Предположим, что, как показано ниже на рисунке 1, в вашей системе есть два устройства, которые должны взаимодействовать друг с другом.

Рисунок 1 – Система из двух взаимодействующих устройств
Рисунок 1 – Система из двух взаимодействующих устройств

«Устройство A» должно отправлять информацию о состоянии, скорости и положении на «Устройство B». Информация о состоянии состоит из трех переменных, которые указывают заряд аккумулятора, режим работы и температуру окружающей среды. Положение представлено двумя переменными, которые показывают положения по осям x и y. Наконец, скорость представлена одной переменной. Предположим, что размер этих переменных такой, как показано в следующей таблице.

Описание переменных системы
Имя переменнойРазмер (байт)Описание
power1Заряд аккумулятора
op_mode1Режим работы
temp1Температура
x_pos2Положение X
y_pos2Положение Y
vel2Скорость

Если «Устройство B» постоянно нуждается в каждой части этой информации, мы можем сохранить все эти переменные в структуре и отправить структуру в «Устройство B». Размер структуры будет, по меньшей мере, таким же большим, как сумма размеров этих переменных, то есть девять байтов.

Таким образом, каждый раз, когда «Устройство A» связывается с «Устройством B», ему необходимо передать 9-байтовый кадр данных через линию связи между двумя этими устройствами. На рисунке 2 изображена структура, которую «Устройство A» использует для хранения переменных, и кадр данных, который должен пройти через канал связи.

Рисунок 2 – Структура, которую «Устройство A» использует для хранения переменных, и кадр данных, который должен пройти через канал связи
Рисунок 2 – Структура, которую «Устройство A» использует для хранения переменных, и кадр данных, который должен пройти через канал связи

Однако давайте рассмотрим другой сценарий, в котором нам только иногда нужно отправлять информацию о состоянии. Кроме того, предположим, что нет необходимости в один момент времени иметь информацию сразу и о скорости, и о положении. Другим словами, иногда мы отправляем только положение, иногда только скорость, и иногда только информацию о состоянии. В такой ситуации хранение информации в девятибайтовой структуре и передача ее в таком виде по линии связи является не очень хорошей идеей.

Информация о состоянии может быть представлена только тремя байтами; для положения и скорости нам понадобятся соответственно только четыре и два байта. Следовательно, максимальное количество байтов, которое «Устройство A» должно отправить за одну передачу, равно четырем, и, следовательно, нам нужно только четыре байта памяти для хранения этой информации. Это четырехбайтовое пространство памяти будет разделено между нашими тремя типами сообщений (рисунок 3).

Кроме того, обратите внимание, что длина кадра данных, передаваемого по линии связи, уменьшена с девяти до четырех байтов.

Рисунок 3 – Хранение и передача только нужной в данный момент информации
Рисунок 3 – Хранение и передача только нужной в данный момент информации

Подводя итог, если в нашей программе есть переменные, которые являются взаимоисключающими, мы можем хранить их в общей области памяти, чтобы сохранить ценное пространство памяти. Это может быть важно особенно в контексте встраиваемых систем с ограниченным объемом памяти. В таких случаях мы можем использовать объединения для создания необходимого общего пространства памяти.

Приведенный выше пример показывает, что использование объединения для обработки взаимоисключающих переменных также может помочь нам сохранить пропускную способность канала связи. Сохранение пропускной способности канала связи иногда даже важнее, чем сохранение памяти.

Использование объединений для пакетов сообщений

Давайте посмотрим, как мы можем использовать объединение для хранения переменных в приведенном выше примере. У нас было три разных типа сообщений: состояние, положение и скорость. Мы можем создать структуры для переменных сообщений состояния и положения (чтобы переменные этих сообщений группировались и обрабатывались как один объект данных).

Для этой цели служат следующие структуры:

struct {
  uint8_t    power;
  unit8_t    op_mode;
  uint8_t    temp;
} status;

struct {
  uint16_t   x_pos;
  unit16_t   y_pos;
} position;

Теперь мы можем поместить эти структуры вместе с переменной "vel" в объединение:

union {
  struct {
    uint8_t    power;
    unit8_t    op_mode;
    uint8_t    temp;
  } status;

  struct {
    uint16_t   x_pos;
    unit16_t   y_pos;
  } position;

  uint16_t     vel;

} msg_union;

Приведенный выше код определяет шаблон объединения и создает переменную этого шаблона (с именем “msg_union”). Внутри этого объединения есть две структуры (“status” и “position”) и одна двухбайтовая переменная (“vel”). Размер этого объединения будет равен размеру его наибольшего члена, а именно, структуры “position”, которая занимает четыре байта памяти. Это пространство памяти распределяется между переменными “status”, “position” и “vel”.

Как отследить активный член объединения

Мы можем использовать общее пространство памяти приведенного выше объединения для хранения наших переменных; однако остается вопрос: как получателю определить, какой тип сообщения был отправлен? Получатель должен распознать тип сообщения, чтобы интерпретировать полученную информацию. Например, если мы отправляем сообщение «положение», все четыре байта полученных данных будут важны, но для сообщения «скорость» следует использовать только два полученных байта.

Чтобы решить эту проблему, нам нужно связать наше объединение с другой переменной, например, “msg_type”, которая указывает тип сообщения (или члена объединения, в который последним была записана информация). Объединение в паре с отдельным значением, указывающим на активный член объединения, называется «меченым объединением» («discriminated union» или «tagged union»).

Что касается типа данных для переменной “msg_type”, для создания символических констант мы можем использовать тип данных языка C перечисление. Однако, чтобы максимально упростить процесс, для указания типа сообщения мы будем использовать символ:

struct {
  uint8_t        msg_type;
  
  union {

    struct {
      uint8_t    power;
      unit8_t    op_mode;
      uint8_t    temp;
    } status;

    struct {
      uint16_t   x_pos;
      unit16_t   y_pos;
    } position;

    uint16_t     vel;

  } msg_union;

} message;

Мы можем рассмотреть три возможных значения для переменной “msg_type”: 's' для сообщения «состояния», 'p' для сообщения «положение» и 'v' для сообщения «скорость». Теперь мы можем отправить структуру “message” «Устройству B» и использовать значение переменной “msg_type” в качестве индикатора типа сообщения. Например, если значение полученного “msg_type” равно 'p', «Устройство B» будет знать, что общее пространство памяти содержит две 2-байтовые переменные.

Обратите внимание, что нам придется добавить еще один байт к кадру данных, отправляемому через канал связи, потому что нам нужно передать переменную “msg_type”. Также обратите внимание, что с этим решением получателю не нужно заранее знать, какое сообщение приходит.

Альтернативное решение: динамическое выделение памяти

Мы увидели, что объединения позволяют нам объявлять общую область памяти, чтобы сохранить пространство памяти и пропускную способность канала связи. Однако существует другой способ хранения взаимоисключающих переменных, таких как переменные в приведенном выше примере. Это второе решение использует динамическое выделение памяти для хранения переменных каждого типа сообщений.

Опять же, нам потребуется переменная “msg_type”, чтобы указать тип сообщения на линии связи, как на стороне передатчика, так и на стороне получателя. Например, если «Устройству A» необходимо отправить сообщение о положении, оно установит “msg_type” в значение 'p' и выделит четыре байта пространства памяти для хранения переменных “x_pos” и “y_pos”. Получатель проверит значение “msg_type” и, в зависимости от его значения, создаст соответствующее пространство памяти для хранения и интерпретации кадра входящих данных.

Использование динамической памяти может быть более эффективным с точки зрения использования памяти, поскольку мы выделяем минимально необходимое место для каждого типа сообщений. С решением на основе объединения это было не так. Там у нас было четыре байта общей памяти для хранения всех трех типов сообщений, хотя для сообщений «состояние» и «скорость» требовалось соответственно только три и два байта. Однако динамическое выделение памяти может быть медленнее, и программист должен включить код, который освобождает выделенную память. Вот почему программисты обычно предпочитают использовать решение с объединением.

Далее: применение объединений

Похоже, что первоначальной целью объединений было создание общей области памяти для взаимоисключающих переменных. Тем не менее, объединения также широко используются для извлечения небольших частей данных из более крупного объекта данных.

Следующая статья этой серии будет посвящена применениям объединений, которые могут быть особенно важны во встраиваемых приложениях.

Теги

Embedded CВысокоуровневые языки программированияПрограммирование для встраиваемых системПрограммирование на CЯзык CЯзык C для встраиваемых систем

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.