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

Наступила полночь и Шехрезада продолжила позволенные речи Для начала замечание к предыдущей статье — я там позволили себе неуважительно высказаться по поводу ASSERT — поясню свою позицию. Мне не очень нравится. что проверка в ASSERT проводится только на этапе исполнения, проверка на этапе компиляции эффективнее. Мне в личку сообщили о существовании конструкции, позволяющей сделать необходимые проверки именно на этапе компиляции (я проверил — действительно работает), думаю, что пост на эту тему скоро появится в песочнице.Итак, мы продолжаем…Как было сказано выше, регистры внешних устройств характеризуются расположением в адресном пространстве (тут мы уже определились) и составом управляющих элементов. Дело в том что многие управляющие элементы недостаточно велики для того, чтобы занять целое слово конкретного МК и, чтобы сэкономить адресное пространство, несколько управляющих элементов могут быть упакованы в один регистр. Такое решение позволяет также в определенных условиях несколько увеличить скорость работы программ, взаимодействующих с данным внешним устройством, потому и встречается весьма часто. В то же время данное решение вызывает определенные проблемы в отношении языка программирования, о которых мы и поговорим.Для примера рассмотрим внешнее устройство, у которого есть регистр управления, причем различные биты регистра выполняют различные функции, а именно младший (нулевой) разряд содержит флаг занятости устройства (0- устройсво свободно и готово выполнять очередную команду, 1 — устройство занято обработкой некоторой команды), а 3 разряда, начиная со второго по старшинству 30…28 (будем считать, что у нас 32х разрядное слово), содержат код команды (если вам такая конструкция показалась не логичной, то вы не видели настоящих описаний регистров). Как же мы можем организовать обращение к отдельным полям такого регистра средствами языка C?… Причем если мы изменяем какое-либо одно поле, остальные биты регистра изменяться не должны.Первое, что приходит в голову (и часто единственное, что там остается) — это битовые маски. Для вышеприведенного случая у нас появится что то вроде: #define IO_STATUS_CMD_MASK 0×70000000 #define IO_STATUS_CMD_BIT 0×28 #define IO_STATUS_FLAG_MASK 0×01 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) <<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) и часть смежных вопросов вынесу в третью часть.

© Habrahabr.ru