Работа с регистрами внешних устройств в языке C, часть 3
Все хорошо, что хорошо кончается
Теперь, когда мы рассмотрели, как с помощью средств языка С мы сможем определить фиксированное расположение регистра в адресном пространстве МК (часть 1), как мы сможем определить отдельные битовые группы в регистре (часть 2), самое время рассмотреть как мы можем с этими группами работать. Работа с группой битов, как с целым, не представляет никаких проблем, опирается на их описание в виде битовых полей и уже демонстрировалась, однако нам может потребоваться и работа с отдельными битами поля, причем по соображениям эффективности либо понятности программы разделять группу на отдельные поля нецелесообразно.Допустим, что нам необходимо отдельно манипулировать старшим битом поля команды из нашего примера. Первое, что приходит в голову, это union, однако объединения не могут иметь битовую длину. Есть вариант создать две версии описания регистров и уже их объединить, и он работает:
#pragma bitfields=reversed
typedef struct {
unsigned:1;
unsigned int code:3;
unsigned:26;
const unsigned flag1:1;
unsigned flag:1;
} tIO_STATUS;
typedef struct {
unsigned:1;
unsigned int start:1;
unsigned:30;
} tIO_STATUSA;
#pragma bitfields=default
typedef union {
tIO_STATUS;
tIO_STATUSA;
} tIO_STATUS2;
#define IO_ADR 0×20000004
volatile tIO_STATUS2 * const pio_device = (tIO_STATUS2 *) (IO_ADR);
pio_device→code = 3;
while (pio_device→flag) {};
pio_device→start=1;
, но создание двух дополнительных типов несколько избыточно (на мой взгляд).Альтернативой для манипуляций отдельным битами группы являются все те же битовые маски и мы приходим к кострукциям типа:
#define BITNUM 2 // биты в группе нумеруются с 0
#define BITMASK (1<
Теперь поговорим о константах. Как правило, для общения с регистрами ВУ существует определенный набор допустимых значений полей и хорошим стилем программирования следует считать описание этих возможностей в виде набора констант с осмысленными именами и проверка на допустимость значения при присвоении (пока что я использовал магическое число 3, но это исключительно в учебных целях). Какие возможности предоставляет нам язык С для решения данной задачи? Их две — определение констант через #define и создание перечислимых типов. Разберем каждую из этих альтернатив. Предположим, что наше устройство способно принимать только 2 команды — «начать работу» с кодом 3 и «прекратить работу» с кодом 2. Тогда мы можем написать:
#define IO_DEVICE_START 3 #define IO_DEVICE_STOP 2 pio_device→code=IO_DEVICE_START; , что чаще всего и делается. Итак магическое число исчезло, даже проверка есть на соответствие размеру битового поля, но выражение pio_device→code=1; компилятором будет пропущено, как допустимое. То есть задача контроля значения на допустимость ложится на плечи разработчика и реализуется ASSERTом. Метод вполне работоспособен, часто применяем и вполне приемлем, если бы не было более удобного, а именно применение перечислимого типа: #pragma bitfields=reversed typedef struct { unsigned:1; enum { O_DEVICE_START=3, IO_DEVICE_STOP=2, } code:3; unsigned:26; const unsigned flag1:1; unsigned flag:1; } tIO_STATUS; #pragma bitfields=default pio_device→code=IO_DEVICE_START; SETBIT (pio_device→code, BITMASK); pio_device→code |= BITMASK; pio_device→code=pio_device | BITMASK; Обратим внимание на то, что в последней строке мы получим предупреждение о несовместимости типов, а в двух предыдущих, которые делают то же самое, не получим (это не баг, это фича такая). Почему же этот метод удобнее? Во-первых мы можем разместить перечисление возможных значений прямо в теле описания структуры, что более читаемо. Во-вторых, компилятор проверит значения в определениях и не позволит выйти нам за рамки размера поля. В-третьих, и это главное, компилятор не позволит нам присвоить полю недопустимое (не указаное в списке) значение, хотя оставляет нам лазейку, показанную в предпоследней строке (если кто знает, как ее закрыть, напишите). Короче, все чудесно и замечательно, НО использовать такую конструкцию вы сможете не во всяком компиляторе, поскольку стандарт С не разрешает использовать для битовых полей ничего, кроме int. Кроме того, даже в IAR потребуется дополнительная директива компилятора --enum_is_int для обеспечения правильного выравнивания. Но если Вас не пугает компиляторозависимость, то метод очень красивый, прозрачный и удобный (заранее соглашаюсь с теми, кто напишет в коментах, что это сильно сократит возможности портирования).Ну и в заключение несколько мыслей по поводу функций и оберток к ним. Часто при просмотре бибилиотек вы встретите что то похожее на следующее:
dev_data_r_w (int n, int data_command, int r_w, int *adr) { … }; int dev_data (int n, int data_comand, iint *adr) { return dev_data_r_w (n, 1, 1, int *adr); int read_dev (int n, int *adr) { return dev_data (n,1, adr); }; int ch_read_dev (int *adr) { return read_dev (1, adr); }; , причем нетрудно видеть, что настоящую работу делает первая функция, а все остальные создают обертки для нее, чтобы не писать соответствующие константные параметры. В языке С++ (и в ряде других) подобная проблема снята значениями параметров по умолчанию, но для С все еще актуальна. Мое личное мнение — так делать не следует. Если не требуется динамическое преобразование типов, то для создания удобных (простых в обращении) синонимов общей функции используйте макросы: #define dev_data (N, DC, ADR) dev_data_r_w ((N),(DC),1,(ADR)) #define read_dev (N, ADR) dev_data ((N),1,(ADR)) #define ch_read_dev (ADR) read_dev (1,(ADR)) Такое определение не сложнее, слегка проигрывает по размеру кода, но выигрывает по времени исполнения и размеру используемой памяти. Особенно умиляют подобные многозвенные конструкции в подпрограммах обработки прерываний. И еще одно наблюдение — некоторые программисты почему-то (если среди читателей будут такие, напишите почему) считают, что создание своего перечислимого типа enum { SET=1, RESET=0 } ACTIVE; — это круто. Я еще могу понять, когда такой тип используется для записи значения в бит, но когда для контроля его значения? Мне кажется, что тип bool вполне такой тип заменяет, хотя кто знает, готов выслушать и иные мнения.Подвожу итог третьей части статьи — целью было договорится о некоторых общих правилах в описании методов доступа к регистрам ВУ, выработать некоторый словарь, поскольку я думаю о написании нескольких постов, в которых шаг за шагом разобрать построение подпрограмм обслуживания периферийных устройств МК от самых простых (SPI, UART) (хотя при глубоком рассмотрении совсем простых устройств не так и много) до достаточно сложных (USB, Ethernet). В принципе задача выполнена, осталось еще ряд замечаний по оформлению программ, но их буду излагать уже по ходу дела.