Работа с регистрами внешних устройств в языке C, часть 1
Вдохновленный несомненным успехом предыдущего поста (никто не написал, что статья неинтересная и не предназначена для Хабра — это уже успех, а многие люди прочитали, написали комментарии и дали советы по оформлению — еще больший успех, кстати, всем спасибо), решил продолжить делиться своими мыслями по поводу программирования МК. Сегодняшние заметки посвящены общим вопросам программирования в языке C, а именно работе с битовыми полями безотносительно к конкретным МК и средам программирования (хотя примеры и будут приводиться для конкретного CORTEX-M1 и IAR). Вроде бы тема не новая, но хотелось бы показать недостатки и достоинства разных методов. Итак, мы начинаем…В программировании МК на языке высокого уровня есть постоянно возникающая задача взаимодействия с регистрами внешних устройств (мне кажется что embedded тем и характеризуется). Прежде всего для организации этого взаимодействия данные регистры необходимо как-то обозначить средствами используемого языка (давайте предположим что это C). Любой регистр ВУ характеризуется своим адресом и составом, каковые и должны быть выражены средствами языка. Сразу же заметим, что для указания конкретного адреса расположения в памяти переменной стандарт С никаких возможностей не представляет (по крайней мере я о таких не знаю), поэтому либо необходимо использовать расширения стандарта, либо применять трюки. Предположим, что нам необходимо записать в 32-х разрядный регистр внешнего устройства, расположенный по адресы 0×40000004, значение 3. Следующий небольшой костылик позволит нам это сделать средствами языка: *(uint32_t *) (0×40000004)=3; Рассмотрим эту строку повнимательнее. Где то выше (в файле stdint.h) есть определение typedef unsigned int uint32_t; , которое позволяет нам далее не задумываться о представлении 32х разрядных чисел в нашей версии С компилятора. Если нам придется перейти на другой вариант компилятора, то у нее будет свой собственный stdint файл и у нас не возникнет вопросов с переносимостью. Такая практика является весьма полезной, и я могу только присоединится к авторам, настоятельно рекомендующим ее использование в embedded программировании.Теперь разберем эту строку справа налево- мы создаем константу, предлагаем компилятору считать ее ссылкой на 32х разрядное число и проводим разименование, обращаясь к области памяти, на которую указывает константа, получая требуемый результат. Полученная конструкция не очень красива: во-первых, используется магическое число, во-вторых, бросается в глаза некоторая исскуственность. Перепишем немного покрасивее: #define IO_DATA_ADRESS 0×40000004 #define WORD (ADR) *(uint32_t *) (ADR) WORD (IO_DATA_ADRESS)=3; Тут все уже почти хорошо, единственное, что не здорово — необходимость использовать макрос в тексте, поэтому (естественно) добавим еще макрос: #define IO_DATA WORD (IO_DATA_ADRESS) IO_DATA=3; Сразу же отвечу тем, кто пожелает эти макросы свернуть в один, особенно учитывая мою нелюбовь к оберточным функциям — макросы НЕ СТОЯТ НИЧЕГО во время исполнения. Вы можете вкладывать сколь угодно макросов внутрь друга друга, и при этом все будет обработано компилятором и в результирующий код упадет одна-единственная константа — результат свертки макросов. Ну, а увеличение времени компиляции настолько незначительно, что вы никогда его не заметите. Конечно не следует злоупотреблять данным обстоятельством (как говорится, без фанатизма), но если применение вложенных макросов делает код понятнее — используйте их не задумываясь, иначе вы рискуете через год-два смотреть в свой же код и лихорадочно пытаться понять, что в нем вообще происходит (а патч с новыми функциями надо представить заказчику уже завтра).Мы получили вполне работоспособный код, причем все реализовали стандартными средствами языка, казалось бы чего лучше? Тем не менее можно и лучше (ну мне так больше нравится) — если мы посмотрим код после препроцессора, то любое обращение к нашему регистру будет превращаться в развернутом виде в ту же самую некрасивую строку с двумя звездочками. И тут нам на помощь приходят (нет, не Чип и Дэйл) указатели. Рассмотрим следующий код uint32_t *pIO_DATA = (uint32_t *) (IO_DATA_ADRESS); *pIO_DATA=3; Теперь при обращении к регистру никаких макросов вообще нет, все выражено средствами языка, код абсолютно прозрачный и (на мой взгляд) более логичный. Единственно, что осталось — не очень нужная звездочка, но об этом чуть позже.Пока что отмечу один недостаток обоих вариантов подобной реализации — никто и ничто не может помешать нам написать #define IO_DATA_ADRESS 0×40000003 и получить исключение во время исполнения программы, поскольку преобразование типов компилятор НИКАК не проверяет и попасть в ногу не мешает (это С, детка, а не ADA, поверь). Уменьшить длину веревки можно при помощи ASSERTов, но их, честно говоря, пишут не всегда, не везде и в недостаточном количестве.Что касается эффективности выполнения обоих конструкций (те, кто читал мои посты, уже поняли, что это мой пунктик), то на моем компиляторе (IAR C/C++ Compiler for ARM 6.60.1.5097) вариант с указателем получается более длительным (из за избыточного индексирования), что лечится применением следующей конструкции uint32_t * const pIO_DATA = (uint32_t *) (IO_DATA_ADRESS); , после чего результаты работы компилятора становятся неразличимыми. LDR.N R0, DATA_TABLE1 MOVS R1,#3 STR R1,[R0] … DATA_TABLE: DC32 0×40000004 Кстати, добавление ключевого слова const соответствует хорошему стилю программирования, поскольку наш указатель очевидно неизменен, а также спасет нас от обидных (и долго разыскиваемых) ошибок типа: pIO_DATA=&i; В таком виде наш способ работы с регистрами вполне хорош, и если бы не недостаток с отсутствием проверки значений, то почти идеален (как известно идеальных вещей не бывает, но почти). Тем не менее, проблемма есть и я с радостью покажу, как она решается (отличный повод блеснуть знаниями). В расширениях языка С, ориентированных на работу с МК, вводят средства указания абсолютных значений адреса. В моем случае это оператор @ (и директива #pragma location), который можно продемонстрировать на следующем примере uint32_t io_data @ IO_DATA_ADRESS; uint32_t * const pIO_DATA = &io_data; i0_data=3; *pIO_DATA=3; Вот в этом варианте нам удается задействовать компилятор для проверки адреса и при попытка ввести не выровненное на слово значение мы получаем (тадам!) сообщение об ошибке (мелочь, а приятно). Эффективность данной конструкции такая же, как и предыдущей, и, если бы не компиляторозависимость (интересное слово получилось), то ее следовало бы рекомендовать для применения. А так все таки, скрепя сердце, выбираем вариант с преобразованием типа и указателем. Читателю предлагается написать макрос, который будет реализовывать тот или иной вариант, в зависимости от некоторого флага.Теперь рассмотрим его единственный недостаток, а именно лишнюю звездочку, и превратим недостаток в неоспоримое достоинство (следите за руками). Как известно программистам МК, устройства, взаимодейcтвие с которыми осуществляется только через один регистр, не существуют встречаются крайне редко в природе. Как правило, существует целый набор регистров для управления устройством и сообщения его состояния, причем они обычно расположены рядом в адресном пространстве МК. Предположим, что наше устройство имеет регистр состояния по адресу 0×40000008, и прежде чем записывать данные, необходимо убедится, что в этом регистре находится ноль. Конечно, никто не мешает нам определить каждый регистр в отдельности и работать с ними как с несвязанными объектами: #define IO_DATA_ADRESS 0×40000004 #define IO_TATUS_ADRESS 0×40000008 (лучше все-таки #define IO_STATUS_ADRESS IO_DATA_ADRESS +4) uint32_t pIO_DATA = (uint32_t *) (IO_DATA_ADRESS); uint32_t pIO_STATUS = (uint32_t *) (IO_STATUS_ADRESS); while {*p IO_STATUS) {}; *pIO_DATA+3; Однако существует более интересный и логически обоснованный способ, а именно, создать структуру, членами которой являются отдельные регистры. В этом случае мы уже на уровне кода понимаем наличие связи между регистрами, ведь их не просто так собрали вместе (если автор программы не идиот —, но эту версию оставим на потомтот случай, когда остальные объяснения откинуты), что способствует пониманию логики работы программы. Итак, что же получается: #define IO_DATA_ADRESS 0×40000004 typedef struct { uint32_t data; uint32_t status; } IO_DEVICE; IO_DEVICE * const pio_device = (IO_DEVICE *) (IO_DATA_ADRESS); while (pio_device→sost==0) {}; pio_device→data=3; , причем быстродействие опять-таки не пострадало, а даже и чуть выросло, поскольку компилятор держит указатель в регистре для и для второй команды его не загружает. Единственный недостаток этого метода — адреса регистров действительно дожны быть рядом, в идеале следовать вплотную, хотя пропуск можно организовать вставлением в структуру пустых полей. Другой недостаток — мы всемерно полагаемся на компилятор в плане упаковки наших полей в реальные адреса и должны четко представлять требования к выравниванию данных.Что то многовато получилось по поводу адресации, поэтому работу с битовыми полями рассмотрим в части 2, если тема интересная.
