Почему запись в объединение бит-поле-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(&reg_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
Другие вопросы по тегам