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

в 7:31, , рубрики: микроконтроллер, Программинг микроконтроллеров, Программирование, метки:

Вдохновленный несомненным успехом предыдущего поста (никто не написал, что статья неинтересная и не предназначена для Хабра — это уже успех, а многие люди прочитали, написали комментарии и дали советы по оформлению — еще больший успех, кстати, всем спасибо), решил продолжить делиться своими мыслями по поводу программирования МК. Сегодняшние заметки посвящены общим вопросам программирования в языке C, а именно работе с битовыми полями безотносительно к конкретным МК и средам программирования (хотя примеры и будут приводиться для конкретного CORTEX-M1 и IAR). Вроде бы тема не новая, но хотелось бы показать недостатки и достоинства разных методов. Итак, мы начинаем…

В программировании МК на языке высокого уровня есть постоянно возникающая задача взаимодействия с регистрами внешних устройств (мне кажется что embedded тем и характеризуется). Прежде всего для организации этого взаимодействия данные регистры необходимо как-то обозначить средствами используемого языка (давайте предположим что это C). Любой регистр ВУ характеризуется своим адресом и составом, каковые и должны быть выражены средствами языка. Сразу же заметим, что для указания конкретного адреса расположения в памяти переменной стандарт С никаких возможностей не представляет (по крайней мере я о таких не знаю), поэтому либо необходимо использовать расширения стандарта, либо применять трюки. Предположим, что нам необходимо записать в 32-х разрядный регистр внешнего устройства, расположенный по адресы 0х40000004, значение 3. Следующий небольшой костылик позволит нам это сделать средствами языка:

*(uint32_t *) (0x40000004)=3;

Рассмотрим эту строку повнимательнее. Где то выше (в файле stdint.h) есть определение

typedef unsigned int uint32_t;

, которое позволяет нам далее не задумываться о представлении 32х разрядных чисел в нашей версии С компилятора. Если нам придется перейти на другой вариант компилятора, то у нее будет свой собственный stdint файл и у нас не возникнет вопросов с переносимостью. Такая практика является весьма полезной, и я могу только присоединится к авторам, настоятельно рекомендующим ее использование в embedded программировании.
Теперь разберем эту строку справа налево- мы создаем константу, предлагаем компилятору считать ее ссылкой на 32х разрядное число и проводим разименование, обращаясь к области памяти, на которую указывает константа, получая требуемый результат. Полученная конструкция не очень красива: во-первых, используется магическое число, во-вторых, бросается в глаза некоторая исскуственность. Перепишем немного покрасивее:

#define IO_DATA_ADRESS  0x40000004 
#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  0x40000003 

и получить исключение во время исполнения программы, поскольку преобразование типов компилятор НИКАК не проверяет и попасть в ногу не мешает (это С, детка, а не 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 0x40000004

Кстати, добавление ключевого слова 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  0x40000004
#define IO_TATUS_ADRESS 0x40000008 (лучше все-таки #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  0x40000004
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, если тема интересная.

Автор: GarryC

Источник

Поделиться

* - обязательные к заполнению поля