Почему запись в объединение бит-поле-uint по ссылке дает неправильную инструкцию по сборке?
Сначала немного предыстории:
Эта проблема возникла при написании драйвера для датчика в моей встроенной системе (STM32 ARM Cortex-M4).
Составитель: ARM NONE EABI GCC 7.2.1
Наилучшим решением для представления регистра внутреннего контроля датчика было использование объединения с битовым полем по этим направлениям.
enum FlagA {
kFlagA_OFF,
kFlagA_ON,
};
enum FlagB {
kFlagB_OFF,
kFlagB_ON,
};
enum OptsA {
kOptsA_A,
kOptsA_B,
.
.
.
kOptsA_G // = 7
};
union ControlReg {
struct {
uint16_t RESERVED1 : 1;
FlagA flag_a : 1;
uint16_t RESERVED2 : 7;
OptsA opts_a : 3;
FlagB flag_b : 1;
uint16_t RESERVED3 : 3;
} u;
uint16_t reg;
};
Это позволяет мне обращаться к битам регистра индивидуально (например, ctrl_reg.u.flag_a = kFlagA_OFF;
), и это позволяет мне устанавливать значение всего регистра сразу (например, ctrl_reg.reg = 0xbeef;
).
Эта проблема:
При попытке заполнить регистр значением, извлеченным из датчика посредством вызова функции, передавая объединение по указателю, а затем обновляя только opts_a
часть регистра перед записью его обратно в датчик (как показано ниже), компилятор генерирует неверный bitfield insert
Инструкция по монтажу.
ControlReg ctrl_reg;
readRegister(&ctrl_reg.reg);
ctrl_reg.opts_a = kOptsA_B; // <-- line of interest
writeRegister(ctrl_reg.reg);
доходность
ldrb.w r3, [sp, #13]
bfi r3, r8, #1, #3 ;incorrectly writes to bits 1, 2, 3
strb.w r3, [sp, #13]
Однако, когда я использую промежуточную переменную:
uint16_t reg_val = 0;
readRegister(®_val);
ControlReg ctrl_reg;
ctrl_reg.reg = reg_val;
ctrl_reg.opts_a = kOptsA_B; // <-- line of interest
writeRegister(ctrl_reg.reg);
Это дает правильную инструкцию:
bfi r7, r8, #9, #3 ;sets the proper bits 9, 10, 11
Функция readRegister не делает ничего интересного и просто записывает в память по указателю
void readRegister(uint16_t* out) {
uint8_t data_in[3];
...
*out = (data_in[0] << 8) | data_in[1];
}
Почему компилятор неправильно устанавливает начальный бит инструкции вставки битового поля?
2 ответа
Я не фанат битовых полей, особенно если вы стремитесь к мобильности. C оставляет гораздо больше неопределенных или определяемых реализацией о них, чем кажется большинству людей, и существуют некоторые очень распространенные заблуждения о том, что требует от них стандарт, а не поведение некоторых реализаций. Тем не менее, это в основном спорный вопрос, если вы пишете код только для конкретного приложения, ориентируясь на одну конкретную реализацию C для целевой платформы.
В любом случае, C не позволяет пространству для соответствующей реализации вести себя непоследовательно для соответствующего кода. В вашем случае одинаково верно установить ctrl_reg.reg
через указатель, в функции readRegister()
, как установить его через назначение. Сделав это, можно назначить ctrl_reg.u.opts_a
, и результат должен правильно прочитать обратно из ctrl_reg.u
, Также разрешено впоследствии читать ctrl_reg.reg
, и это будет отражать результат модификации.
Однако вы делаете предположения о расположении битовых полей, которые не поддерживаются стандартом. Ваш компилятор будет непротиворечивым, но вам нужно тщательно проверить, что компоновка действительно соответствует вашим ожиданиям, иначе переход туда-сюда между двумя членами союза не даст желаемого результата.
Тем не менее, способ хранения значения в ctrl_reg.reg
несущественно в отношении эффекта, который имеет присвоение битовому полю. Ваш компилятор не обязан генерировать одинаковую сборку для двух случаев, но если между этими двумя программами нет других отличий, и они не выполняют неопределенного поведения, то они должны генерировать одинаковое наблюдаемое поведение для одних и тех же входных данных.
Это 100% правильный код, сгенерированный компилятором
void foo(ControlReg *reg)
{
reg -> opts_a = kOptsA_B;
}
void foo1(ControlReg *reg)
{
volatile ControlReg reg1;
reg1.opts_a = kOptsA_B;
}
foo:
movs r2, #1
ldrb r3, [r0, #1] @ zero_extendqisi2
bfi r3, r2, #1, #3
strb r3, [r0, #1]
bx lr
foo1:
movs r2, #1
sub sp, sp, #8
ldrh r3, [sp, #4]
bfi r3, r2, #9, #3
strh r3, [sp, #4] @ movhi
add sp, sp, #8
bx lr
Как вы видите в функции 'foo', она загружает только один байт (второй байт объединения), и поле хранится в 1-3 битах этого байта.
Как вы видите в функции 'foo1', она загружает половину слова (всю структуру), и поле хранится в 9-11 битах половинного слова.
Не пытайтесь найти ошибки в компиляторах, потому что они почти всегда присутствуют в вашем коде.
PS
Вам не нужно называть структуру и битовые поля заполнения
typedef union {
struct {
uint16_t : 1;
uint16_t flag_a : 1;
uint16_t : 7;
uint16_t opts_a : 3;
uint16_t flag_b : 1;
uint16_t : 3;
};
uint16_t reg;
}ControlReg ;
РЕДАКТИРОВАТЬ
но если вы хотите убедиться, что вся структура (объединение) изменена, просто сделайте параметр функции volatile
void foo(volatile ControlReg *reg)
{
reg -> opts_a = kOptsA_B;
}
foo:
movs r2, #1
ldrh r3, [r0]
bfi r3, r2, #9, #3
strh r3, [r0] @ movhi
bx lr