Стандарт C не определяет, сколько памяти занимает последовательность битовых полей или в каком порядке находятся битовые поля. В вашем примере некоторые компиляторы могут решить использовать 32-битные битовые поля, даже если выясно ожидайте, что это покроет 16 битов.Поэтому использование битовых полей блокирует вас до определенного компилятора и определенных флагов компиляции.
Использование типов, больших чем unsigned char
, также имеет эффекты, определяемые реализацией, но на практике это гораздо более переносимо.В реальном мире для uintNN_t
есть только два варианта: с прямым порядком байтов или с прямым порядком байтов, и обычно для данного ЦП все используют один и тот же порядок, потому что это тот порядок, который ЦП использует изначально.(Некоторые архитектуры, такие как mips и arm, поддерживают оба порядковых номера, но обычно люди придерживаются одного порядкового номера в широком диапазоне моделей ЦП.) Если вы обращаетесь к собственным регистрам ЦП, его порядковый номер в любом случае может быть частью ЦП.С другой стороны, если вы обращаетесь к периферийному устройству, вам нужно позаботиться.
В документации к устройству, к которому вы обращаетесь, будет указано, какой объем памяти нужно адресовать одновременно (очевидно, 2байт в вашем примере) и как расположены биты.Например, в нем может быть указано, что регистр является 16-битным регистром, доступ к которому осуществляется с помощью 16-битных инструкций загрузки / сохранения независимо от того, что является порядковым номером процессора, что data1
охватывает 5 младших битов, data2
охватывает следующие 3, data3
следующие 4 и data4
следующие 4. В этом случае вы должны объявить регистр как uint16_t
.
typedef volatile uint16_t data_port_t;
data_port_t *port = GET_DATA_PORT_ADDRESS();
Адреса памяти в устройствах почти всегда должны быть объявлены volatile
, потому что важно, чтобы компилятор читал и записывал их в нужное время.
Чтобы получить доступ к частям регистра, используйте операторы bit-shift и bit-mask.Например:
#define DATA2_WIDTH 3
#define DATA2_OFFSET 5
#define DATA2_MAX (((uint16_t)1 << DATA2_WIDTH) - 1) // in binary: 0000000000000111
#define DATA2_MASK (DATA2_MAX << DATA2_OFFSET) // in binary: 0000000011100000
void set_data2(data_port_t *port, unsigned new_field_value)
{
assert(new_field_value <= DATA2_MAX);
uint16_t old_register_value = *port;
// First, mask out the data2 bits from the current register value.
uint16_t new_register_value = (old_register_value & ~DATA2_MASK);
// Then mask in the new value for data2.
new_register_value |= (new_field_value << DATA2_OFFSET);
*port = new_register_value;
}
Очевидно, вы можете сделать код намного короче.Я разделил его на отдельные крошечные шаги, чтобы за логикой было легко следовать.Я включаю более короткую версию ниже.Любой компилятор, достойный своей соли, должен компилировать тот же код, за исключением режима без оптимизации.Обратите внимание, что выше, я использовал промежуточную переменную вместо двух назначений для *port
, потому что выполнение двух назначений для *port
изменило бы поведение: это заставило бы устройство видеть промежуточное значение (и другое чтение, так как |=
это и чтение, и запись).Вот более короткая версия и функция чтения:
void set_data2(data_port_t *port, unsigned new_field_value)
{
assert(new_field_value <= DATA2_MAX);
*port = (*port & ~(((uint16_t)1 << DATA2_WIDTH) - 1) << DATA2_OFFSET))
| (new_field_value << DATA2_OFFSET);
}
unsigned get_data2(data_port *port)
{
return (*port >> DATA2_OFFSET) & DATA2_MASK;
}
#define CAN0 (*(CAN_REG_FILE *)CAN_BASE_ADDRESS)
Здесь нет функции.Объявление функции должно иметь тип возврата, за которым следует список аргументов в скобках.Это принимает значение CAN_BASE_ADDRESS
, которое, по-видимому, является указателем некоторого типа, затем приводит указатель к указателю на CAN_REG_FILE
и, наконец, разыменовывает указатель.Другими словами, он обращается к файлу регистрации CAN по адресу, указанному CAN_BASE_ADDRESS
.Например, могут быть объявления типа
void *CAN_BASE_ADDRESS = (void*)0x12345678;
typedef struct {
const volatile uint32_t status;
volatile uint16_t foo;
volatile uint16_t bar;
} CAN_REG_FILE;
#define CAN0 (*(CAN_REG_FILE *)CAN_BASE_ADDRESS)
, а затем вы можете делать такие вещи, как
CAN0.foo = 42;
printf("CAN0 status: %d\n", (int)CAN0.status);