Pull to refresh

Работа с регистрами внешних устройств в языке C, часть 2

Reading time8 min
Views10K
Наступила полночь и Шехрезада продолжила позволенные речи

Для начала замечание к предыдущей статье — я там позволили себе неуважительно высказаться по поводу ASSERT — поясню свою позицию. Мне не очень нравится. что проверка в ASSERT проводится только на этапе исполнения, проверка на этапе компиляции эффективнее. Мне в личку сообщили о существовании конструкции, позволяющей сделать необходимые проверки именно на этапе компиляции (я проверил — действительно работает), думаю, что пост на эту тему скоро появится в песочнице.
Итак, мы продолжаем…
Как было сказано выше, регистры внешних устройств характеризуются расположением в адресном пространстве (тут мы уже определились) и составом управляющих элементов. Дело в том что многие управляющие элементы недостаточно велики для того, чтобы занять целое слово конкретного МК и, чтобы сэкономить адресное пространство, несколько управляющих элементов могут быть упакованы в один регистр. Такое решение позволяет также в определенных условиях несколько увеличить скорость работы программ, взаимодействующих с данным внешним устройством, потому и встречается весьма часто. В то же время данное решение вызывает определенные проблемы в отношении языка программирования, о которых мы и поговорим.

Для примера рассмотрим внешнее устройство, у которого есть регистр управления, причем различные биты регистра выполняют различные функции, а именно младший (нулевой) разряд содержит флаг занятости устройства (0- устройсво свободно и готово выполнять очередную команду, 1 — устройство занято обработкой некоторой команды), а 3 разряда, начиная со второго по старшинству 30..28 (будем считать, что у нас 32х разрядное слово), содержат код команды ( если вам такая конструкция показалась не логичной, то вы не видели настоящих описаний регистров). Как же мы можем организовать обращение к отдельным полям такого регистра средствами языка C?.. Причем если мы изменяем какое-либо одно поле, остальные биты регистра изменяться не должны.
Первое, что приходит в голову (и часто единственное, что там остается) — это битовые маски. Для вышеприведенного случая у нас появится что то вроде:
#define IO_STATUS_CMD_MASK 0x70000000
#define IO_STATUS_CMD_BIT 28
#define IO_STATUS_FLAG_MASK 0x01
while (*pio_status & IIO_STATUS_FLAG) {};
*pio_status=(*pio_status & ~IO_STATUS_CMD_MASK) | ((command << IO_STATUS_CMD_BIT) & IO_STATUS_CMD_MASK);
Наверное, какие то из скобок здесь излишни, если помнить очередность выполнения операций, но я всегда придерживался мнения что скобки опять-таки ничего не стоят на этапе выполнения, и проще поставить лишнюю пару, чем запоминать соответствующие таблицы приоритетов или не дай бог ошибиться. Что же мы видим? В первой строке мы сообщаем компилятору, что три подряд стоящих бита (чисто теоретически могут существовать поля из не подряд стоящих битов, но это уже сильно похоже на извращение, а мы все-таки нормальные люди) образуют единое поле.
Следующая строка сообщает номер младшего разряда этого поля. Третья строка определяет битовое поле из одного бита, для него мы определять номер младшего ( единственного) бита не будем, поскольку он нам не потребуется. В четвертой строке мы из регистра выделяем необходимый нам (единственный) бит и проверяем его единичность. А вот пятая строка выглядит несколько угрожаеще, но тоже не сложна — разбираем слева-направо: берем текущее значение регистра, оставляем в нем ВСЕ биты, КРОМЕ тех, что должны изменить, берем новое значение поля, сдвигаем его влево на номер младшего бита поля, оставляем в полученном результате ТОЛЬКО ТЕ биты, которые хотим изменить, и объединяем два полученных значения, получая то, что нам требуется. Чтобы не писать всякий раз последнюю строку, мы можем создать макрос
#define DATA_SET(ADR,MASK,BIT,DATA) (ADR)=((ADR) & ~(MASK)) | (((DATA) <<(BIT)) & (MASK))
DATA_SET(*pio_status,IO_STATUS_CMD_MASK,IO_STATUS_CMD_BIT,command); 
Поскольку пост на этом не закончился, данное решение имеет недостатки — покажем их. Во-первых, первые две строка очевидно взаимосвязаны, тем не менее они существуют различно друг от друга, и мы должны сами следить за их соответствием. Легко написать макрос, который из битовой маски сделает битовую маску с единственным младшим битом
#define LOWBIT(MASK) (((~(MASK)-1)) & (MASK))
#define LOWBIT(MASK) (~(MASK << 1) & (MASK))
#define LOWBIT(MASK) (~(MASK)+1) & (MASK)
, труднее (у меня не получилось), красивый макрос, выделяющий номер младшего бита. Но нам достаточно и такой битовой маски, при этом код макроса передачи значения изменится незначительно
#define DATA_SET(ADR,MASK,DATA) (ADR)=((ADR) & ~(MASK)) | (((DATA) * LOWBIT(MASK)) & (MASK))
DATA_SET(*pio_status,IO_STATUS_CMD_MASK,command); 
Внимательный читатель спросит, а как же эффективность? В старом варианте был сдвиг, который в ARM делается за такт, а в новом умножение (а, как потребуется для чтения, и деление)? Хорошо, если мы устанавливаем константные значения, макрос свернется, а если поле command переменная? К счастью, компиляторы проектируются по-настоящему талантливыми людьми, и если у нас включен хотя бы уровень оптимизации средний, то умножение и деление на константу из одного бита превращается в соответствующее количество сдвигов (по крайней мере у меня именно так). Такой (или аналогичный) вариант кода широко распространен и представлен в разнообразных библиотеках программ для работы с регистрами устройств (BSP). Его недостатки с точки зрения автора — необходимость использования макроса для обращения к полю ( вариант с прямым применением в тексте строки 5 из первого примера я даже не хочу рассматривать — это просто чудовищно) и необходимость очень внимательно следить за применяемыми в макросах значениями масок, что не допустить обидных ошибок типа
DATA_SET(*pio_status,IO_STATUS_FLAG_MASK,command); 
, поскольку компилятор за нас ничего проверить не в силах. Конечно можно написать еще один макрос
#define IO_CMD_SET(DATA) DATA_SET(*pio_status,IO_STATUS_CMD_MASK,DATA) 
IO_CMD_SET(command);
, но очень быстро таких макросов станет многовато и опять возникнет возможность ошибки. Другой недостаток — отсутствие проверки типов, то есть операция вроде
IO_CMD_SET(36);
у компилятора не вызовет никаких сомнений, хотя результат исполнения может вас неприятно удивить.
Ну и теперь, когда мы увидели все недостатки распространенного метода, я прямо таки обязан представить способ описания регистров, их лишенный (но при этом имеющий свои собственные). Итак, на сцене появляются битовые поля, которые довольно описаны в любой книге по С, где они позволяют сэкономить память путем размещения «коротких» объектов в одном слове. В то же время ничто не мешает нам использовать эти языковые средства для описания существующих регистров. При это мы должны предельно четко контролировать процесс упаковки полей в слово, а именно направление упаковки и требования к выравниванию. Для управления этим процессов в рассматриваемом компиляторе существует директива:
#pragma bitfields = disjoint_types // размещение полей, начиная с младших битов слова
#pragma bitfields = reversed // размещение полей, начиная со старших битов слова
Тогда структура рассматриваемого регистра может быть описана следующим образом:
#pragma bitfields=reverse
typedef struct {
  unsigned :1; // пропускаем старший бит (31)
  unsigned code:3; // (30..28)
  unsigned : 27; // пропускаем 27 бит (27..1)
  unsigned flag:1; // (0 - младший бит)
} tIO_STATUS;
#pragma bitfields=default
volatile tIO_STATUS * const pio_status = (tIO_STATUS *) (IO_ADR);
Примечание: предпоследняя строка примера возвращает режим размещения полей в значение по умолчанию, на тот случай, если дальше будут фрагменты, авторы которых на озаботились включением прагмы перед своим определением (но мы то не такие, мы предусмотрительные). Отметим недостаток данного метода — мы должны руками посчитать длину полей для заполнения (об этом чуть позже) и тут же перейдем к перечислению достоинств: мы не должы определять никаких масок и битов (компилятор все сделает за нас), более того, он же проверит записываемые в поля константные данные на допустимость. Доступ к полям регистра теперь осуществляется стандартными языковыми средствами
while (pio_status->flag) {};
poi_status->code=3;
, причем работает автозаполнение и проверка типов, то есть оператор
pio_status->code=34;
получит предупреждение от компилятора.
Теперь об эффективности — не знаю, как на других компиляторах, но на моем (то есть на IAR, конечно) нет никакой разницы в порождаемом коде, то есть битовые поля компилятор превращает в обращение к битовым маскам со всеми необходимыми сдвигами и логическими операциями, но не более того. Более того, для МК, поддерживающих побитовую адресацию, код для работы с однобитовым полем может быть и эффективнее. Сумируя, использование битовых полей дает нам существенно более комфортную работу при отстутствии накладных расходов. Единственный недостаток такого метода — невозможность стандартным образом работать в одном операторе сразу с несколькими полями, что возможно при использовании масок. В то же время операторы типа
*pio_status=(*pio_status & ~(IO_STATUS_CMD_MASK |  IO_STATUS_FLAG_MASK)) | ((command << IO_STATUS_CMD_BIT) & IO_STATUS_CMD_MASK) | (IO_STATUS_FLAG_MASK * flagset);
никоим образом не могут быть рекомендованы к применению ввиду трудностей сопровождения. Тем не менее, если в них есть необходимость, то есть такой одновременный доступ к полям требуется особенностями внешнего устройства, то конструкции типа
#define WORD(adr) *(int*)(adr) 
WORD(pio_status)=WORD(pio_status) & ... 
позволят нам удлинить веревку до требуемого размера, хотя лично я в такой ситуации предпочел бы создать временную структуру, модифицировать ее, и потом переслать в регистр,
tIO_SATUS tmp;
tmp=*pio_status; // чтобы сохранить значение немодифицируемых битов
 tmp.code=command; tmp.flag=flagset; *pio_status=tmp;
но иногда это может быть недопустимо по соображениям эффективности. Еще одно замечание- если вам нужны подобные операторы, то возникнут определенные сложности, если в структуре вы некоторые поля опишете как const
#pragma bitfields=reverse
typedef struct {
  unsigned :1; // пропускаем старший бит (31)
  unsigned code:3; // (30..28)
  unsigned : 27; // пропускаем 26 бит (27..2)
  unsigned const readflag:1; // бит только для чтения (1)
  unsigned flag:1; // (0 - младший бит)
} tIO_STATUS;
#pragma bitfields=default
volatile tIO_STATUS * const pio_status = (tIO_STATUS *) (IO_ADR);
tIO_STATUS tmp={0,0,1}; // нужно присвоение, иначе будет предупреждение о неинициализированной константе
tmp=*pio_status; // ошибка присвоения константному полю
*(int*)(&tmp)=*(int*)pio_device; // так можно
tmp.code=3; tmp.flag=1;
*(int*)(pio_device)=*(int*)(&tmp);
, что выглядит несколько корявенько.
Ну и в заключение об недостатке, связанном с ручным вычислением бит — когда пост задумывался, тут должен был быть шикарный макрос, который получал описание регистра вроде
REGISTR(tIO_STATUS,code[30..28],readflag[1],flag[0])
и существенно упрощал работу программиста, создавая соответствующее описание структуры для последующего использования (те, кто программировали в MASMе, подобные макросы встречали). И здесь меня ожидал неприятный сюрприз — препроцессор языка С не является макроязыком, поскольку лишен целого ряда необходимых возможностей. Это действительно только препроцессор и его способности по обработке текста программы весьма и весьма ограничены. Сначала я не поверил в подобное открытие и где-то час считал себя идиотом, не способным найти информацию. Потом час считал идиотами авторов в Инете и пытался извращенными способами сконструировать нужный мне макрос. В принципе, наверное его все-таки можно сделать при условии двухпроходного препроцессора, но выглядеть будет ужасно. Ну а потом я все-таки осознал, что разработчики препроцессора не ставили перед собой задачи создать макроязык (непонятно почему, но им виднее), так что и они все молодцы и я тоже не так плох. Тем не менее задача не решена — возможно применение внешних препроцессоров типа M4 либо PowerShell, возможно применение скриптовых языков типа Perl, возможен даже собственный минимальный препроцессор в виде Java модуля или даже исполняемого файла, но все это костыли и не представляется красивым и, главное, удобным решением. Если я что то недопонял, подскажите в комментах.
Общий вывод — битовые поля языка С представляют собой эффективный и удобный инструмент описания регистров внешних устройств, который настоятельно рекомендуется к использованию при принятии минимальных мер контроля.
Что-то опять длинно получилось, поэтому вопрос о заполнении полей (#define VS enum) и часть смежных вопросов вынесу в третью часть.
Tags:
Hubs:
+14
Comments28

Articles

Change theme settings