Почему запись в объединение битового поля с помощью ссылки приводит к неправильной инструкции по сборке? - PullRequest
0 голосов
/ 25 января 2019

Сначала немного предыстории:

Эта проблема возникла при написании драйвера для датчика во встроенной системе (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];
}

Почему компилятор неправильноperly установить начальный бит инструкции вставки битового поля?

Ответы [ 2 ]

0 голосов
/ 26 января 2019

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

1002 * В любом случае, C не оставляет места для соответствующей реализации, чтобы вести себя непоследовательнодля соответствия кода.В вашем случае одинаково допустимо установить ctrl_reg.reg через указатель, в функции readRegister(), чтобы установить его через присваивание.Сделав это, можно присвоить ctrl_reg.u.opts_a, и результат должен правильно считываться с ctrl_reg.u.После этого также разрешается читать ctrl_reg.reg, и это будет отражать результат модификации.

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

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

0 голосов
/ 26 января 2019

Это 100% правильный код, сгенерированный компилятором

enter image description here

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

Вам не нужно называть struct и битовые поля заполнения

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 ;

EDIT

, но если вы хотите убедиться, что вся структура (объединение) изменена, просто сделайте параметр функции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
...