Чтение и запись из регистров в STM32 кажется очень неуклюжим и неинтуитивным для чтения, например:
BRvalue = (SPI_CR1_BR & SPI1->CR1) >> SPI_CR1_BR_Pos; // Read current BR value
SPI1->CR1 &= ~SPI_CR1_BR; // Clear bits in BR position.
SPI1->CR1 |= 0b010 << SPI_CR1_BR_Pos; // Write new bits.
Исходя из программирования PIC с помощью XC8, у них есть эти хорошие структуры (внутри объединения, потому что иногда у них есть несколько структур, где они также определяют битовые поля как единичные биты или используют разные слова для именования битов и т. д.), которые позволяют читать и писать сингулярные биты внутри регистров. Мне понравился баланс простоты и читаемости, поэтому я написал, например, свой собственный для этого регистра:
typedef union {
struct {
volatile unsigned CPHA :1;
volatile unsigned CPOL :1;
volatile unsigned MSTR :1;
volatile unsigned BR :3;
volatile unsigned SPE :1;
volatile unsigned LSBFIRST :1;
volatile unsigned SSI :1;
volatile unsigned SSM :1;
volatile unsigned RXONLY :1;
volatile unsigned CRCL :1;
volatile unsigned CRCNEXT :1;
volatile unsigned CRCEN :1;
volatile unsigned BIDIOE :1;
volatile unsigned BIDIMODE :1;
};
} SPI1CR1bits_t;
SPI1CR1bits_t *SPI1CR1bits = (SPI1CR1bits_t*) &(SPI1->CR1);
Это позволяет выполнять операцию чтения/записи НАМНОГО более элегантным способом:
BRvalue = SPI1CR1bits->BR; // Read value
SPI1CR1bits->BR = 0b010; // Write value
Я протестировал его, и он, кажется, работает отлично. Он также работает для чтения битов состояния, которые меняет HW.
Однако я не нашел такой распространенной практики в STM32. Есть ли причина? С какими проблемами я могу столкнуться при чтении/записи с помощью этого метода? Есть ли вещи, которых мне следует опасаться?
Одна вещь: если вы сопоставляете свою структуру непосредственно с аппаратным обеспечением, если вы измените два бита (например, str->SSI = 1; str->SSM = 0;
), ее придется извлекать и сохранять дважды (например, tmp = reg; tmp |= SSI; reg = tmp; tmp = reg; tmp &= ~SSM; reg = tmp;
). Доступ к регистрации может быть немного медленнее. Итак, вы можете удалить volatile
из своей структуры и добавить поле: union { struct { unsigned SSI:1; unsigned SSM:1 } ; unsigned raw; };
Затем выполните: mystruct.raw = SPI_CR1_BR; mystruct.SSI = 1; mystruct.SSM = 0; SPI_CR1_BR = mystruct.raw;
Затем вы извлекаете/сохраняете аппаратный регистр только один раз.
Некоторые аппаратные средства ожидают, что вы получите временную копию регистра, измените биты во временной переменной и запишите обратно все значение регистра. То есть аппаратное обеспечение может ожидать, что несколько бит будут изменены «атомарно» за одну запись в регистр. Зависит от данного аппаратного регистра и того, что сказано в документации.
Причина «почему», скорее всего, заключается в том, что предоставленный заголовок должен работать с несколькими компиляторами, которые не всегда могут вести себя одинаково. Microchip предоставляет свой собственный компилятор, и их заголовок должен работать только с ним. По крайней мере, для Cortex-M3/4 вы могли читать отдельные биты атомарно, используя побитовую обработку. Все ST обеспечивают более высокие уровни абстракции как в библиотеках Cube HAL или LL, так и в более старой (и лучшей IMO) SPL.
Обратите внимание, что Armcc, по крайней мере, будет получать доступ к однобитовым битовым полям, используя побитовую обработку, где это возможно. Другие компиляторы YMMV.
Расположение битовых полей не гарантируется стандартом и зависит от компилятора. Не используйте для этого битовые поля. Вместо этого создайте макросы GET_BIT и SET_BIT.
Использование битовых полей C будет работать лишь частично.
Основная проблема заключается в том, что компиляторы реализуют битовые поля, предполагая, что базовые целочисленные переменные ведут себя как обычная память, которую можно читать и записывать. Таким образом, они реализуют операции записи, сначала читая базовую целочисленную переменную, изменяя соответствующие биты и, наконец, записывая базовую целочисленную переменную. Это предположение справедливо не для всех регистров STM32.
Это работает, если все биты в регистре имеют тип rw
, r
или не используются. Но это не работает, если регистр содержит биты с другим поведением, например rc_w0
, rc_w1
, rc_r
, t
и т. д. В этих случаях запись в битовые поля в большинстве случаев приведет к непредусмотренным изменениям битов.
В справочных руководствах STM32 подробно описано поведение битов регистра, а в первой главе дано объяснение различного поведения битов.
Таким образом, использование битовых полей должно быть ограничено регистрами, содержащими только rw
, r
и неиспользуемые биты.
Менее серьезная проблема заключается в том, что стандарт C не определяет, как биты битового поля сопоставляются с битами базовой целочисленной переменной. Каждый компилятор может реализовать это по-своему. На практике они различаются только в том случае, если структура использует больше битов, чем помещается в одну целочисленную переменную, или если битовые поля в структуре используют разные целочисленные типы данных.
Однако это не должно влиять на регистры STM32, поскольку все они являются 32-битными целыми числами. Поэтому просто последовательно используйте uint32_t
в качестве целочисленного типа данных и создайте отдельную структуру для каждого регистра, и вы будете в безопасности.
но не нашел такой распространенной практики в STM32. Есть ли причина?
IMO, потому что ARM обязала своих лицензиатов использовать стандарт кодирования во всем сопутствующем программном обеспечении (называемом CMSIS; к сожалению, это прозвище слишком часто встречается в рекламных материалах без надлежащего объяснения, поэтому его смысл был несколько потерян).
Итак, производители микроконтроллеров, такие как ST, производили, например, заголовки устройств, описывающие регистры и их биты/битовые поля согласно CMSIS. И CMSIS избегает битовых полей - именно по причинам, указанным @Codo в другом ответе (хотя, по моему мнению, основная причина - расплывчатое определение битовых полей в стандарте C, поскольку проблема с RMW в регистрах, не являющихся чисто памятью, не является битовым полем конкретно - вы будете укушены таким же образом, если будете использовать RMW через обычные операторы |= &= и даже использовать побитовую обработку на Cortex-M3/M4).
Как уже было сказано здесь, пожалуйста, проверьте свойства чтения и записи битов регистра, чтобы избежать непредвиденных эффектов.
Если вы намерены внести несколько изменений в значение регистра, как говорили другие в комментариях, рекомендуется поработать со значением переменной, а затем полностью зафиксировать изменения.
Если вы добавите в поле union
, uint16_t
или uint32_t
, вы сможете читать и писать в реестр без приведения.
Более того, создание собственных битовых структур может быть подвержено ошибкам; легко забыть битовое поле или установить неправильный размер при копировании полей структуры. Я бы рекомендовал, если ваш компилятор позволяет, добавить, например. проверка утверждения, чтобы убедиться, что размер структуры или объединения больше ожидаемого. Очевидно, что эта проверка в любом случае не покроет всех ошибок, которые можно допустить при определении структуры.
// if using e.g. the STM32F76xxx series, the SPIx_CR1 registers can be accessed in 16-bit (half-words) or 32-bit (words) modes
// let'use a 16-bit structure
typedef struct {
volatile unsigned CPHA :1;
volatile unsigned CPOL :1;
volatile unsigned MSTR :1;
volatile unsigned BR :3;
volatile unsigned SPE :1;
volatile unsigned LSBFIRST :1;
volatile unsigned SSI :1;
volatile unsigned SSM :1;
volatile unsigned RXONLY :1;
volatile unsigned CRCL :1;
volatile unsigned CRCNEXT :1;
volatile unsigned CRCEN :1;
volatile unsigned BIDIOE :1;
volatile unsigned BIDIMODE :1;
} __attribute__((packed)) type_struct_register_SPICR1_bits;
// using C11 standard for assert
// checking if we added too many bits
_Static_assert(sizeof(type_struct_register_SPICR1_bits) != 2, "wrong size for type_struct_register_SPICR1_bits");
// union with the bits and the half-word view of the register
typedef union {
type_struct_register_SPICR1_bits bits;
uint16_t reg;
} type_union_16bits_register_SPICR1;
// using C11 standard for assert
// checking if the union has the expected size
_Static_assert(sizeof(type_union_register_SPICR1) != 2, "wrong size for type_union_16bits_register_SPICR1");
type_union_16bits_register_SPICR1 SPI1CR1;
// read without casting
SPI1CR1.reg = SPI1->CR1;
// if you need to make multiple assignments at once, you can do your assignments on the variable
SPI1CR1.bits.BR = 0b010;
SPI1CR1.bits.CPHA = 1;
// write without casting; commits the changes altogether
SPI1->CR1 = SPI1CR1.reg;
Наверное, хорошо. Есть несколько способов определить аппаратные регистры, отображаемые в памяти: некоторые из них — ваш и stm32. Чтобы помочь тем из нас, кто не хочет копать, было бы полезно, если бы вы отредактировали свой вопрос и опубликовали определения stm32 для
SPI_CR1_BR
et. ал. которые полностью отражают то, что вы хотите заменить.