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

  • Tutorial
Вдохновленный несомненным успехом предыдущего поста (никто не написал, что статья неинтересная и не предназначена для Хабра — это уже успех, а многие люди прочитали, написали комментарии и дали советы по оформлению — еще больший успех, кстати, всем спасибо), решил продолжить делиться своими мыслями по поводу программирования МК. Сегодняшние заметки посвящены общим вопросам программирования в языке 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;
Сразу же отвечу тем, кто пожелает эти макросы свернуть в один, особенно учитывая мою нелюбовь к оберточным функциям — макросы НЕ СТОЯТ НИЧЕГО во время исполнения. Вы можете вкладывать сколь угодно макросов внутрь друга друга, и при этом все будет обработано компилятором и в результирующий код упадет одна-единственная константа — результат свертки макросов. Ну а увеличение времени компиляции настолько незначительно, что вы никогда его не заметите. Конечно не следует злоупотреблять данным обстоятельством (как говорится, без фанатизма), но если применение вложенных макросов делает код понятнее — используйте их не задумываясь, иначе вы рискуете через год-два смотреть в свой же код и лихорадочно пытаться понять, что в нем вообще происходит (а патч с новыми функциями надо представить заказчику уже завтра).
Мы получили вполне работоспособный код, причем все реализовали стандартными средствами языка, казалось бы чего лучше? Тем не менее можно и лучше (ну мне так больше нравится) — если мы посмотрим код после препроцессора, то любое обращение к нашему регистру будет превращаться в развернутом виде в ту же самую некрасивую строку с двумя звездочками. И тут нам на помощь приходят (нет, не Чип и Дэйл) указатели. Рассмотрим следующий код
volatile 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) вариант с указателем получается более длительным (из за избыточного индексирования), что лечится применением следующей конструкции
volatile 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), который можно продемонстрировать на следующем примере
volatile uint32_t io_data @ IO_DATA_ADRESS;
volatile uint32_t * const pIO_DATA = &io_data;
i0_data=3; *pIO_DATA=3;
Вот в этом варианте нам удается задействовать компилятор для проверки адреса и при попытка ввести не выровненное на слово значение мы получаем (тадам!) сообщение об ошибке (мелочь, а приятно). Эффективность данной конструкции такая же, как и предыдущей, и, если бы не компиляторозависимость (интересное слово получилось), то ее следовало бы рекомендовать для применения. А так все таки, скрепя сердце, выбираем вариант с преобразованием типа и указателем. Читателю предлагается написать макрос, который будет реализовывать тот или иной вариант, в зависимости от некоторого флага.
Теперь рассмотрим его единственный недостаток, а именно лишнюю звездочку, и превратим недостаток в неоспоримое достоинство (следите за руками). Как известно программистам МК, устройства, взаимодейcтвие с которыми осуществляется только через один регистр,не существуют встречаются крайне редко в природе. Как правило, существует целый набор регистров для управления устройством и сообщения его состояния, причем они обычно расположены рядом в адресном пространстве МК. Предположим, что наше устройство имеет регистр состояния по адресу 0х40000008, и прежде чем записывать данные, необходимо убедится, что в этом регистре находится ноль. Конечно, никто не мешает нам определить каждый регистр в отдельности и работать с ними как с несвязанными объектами:
#define IO_DATA_ADRESS  0x40000004
#define IO_STATUS_ADRESS 0x40000008 (лучше все-таки #define IO_STATUS_ADRESS IO_DATA_ADRESS +4) 
volatile uint32_t pIO_DATA = (uint32_t *) (IO_DATA_ADRESS);
volatile uint32_t pIO_STATUS = (uint32_t *) (IO_STATUS_ADRESS);
while {*pIO_STATUS) {};
*pIO_DATA=3;
Однако существует более интересный и логически обоснованный способ, а именно, создать структуру, членами которой являются отдельные регистры. В этом случае мы уже на уровне кода понимаем наличие связи между регистрами, ведь их не просто так собрали вместе (если автор программы не идиот — но эту версию оставим на потомтот случай, когда остальные объяснения откинуты), что способствует пониманию логики работы программы. Итак, что же получается:
#define IO_DATA_ADRESS  0x40000004
typedef struct  {
   uint32_t data;
   uint32_t status;
} IO_DEVICE;
volatile IO_DEVICE * const pio_device = (IO_DEVICE *) (IO_DATA_ADRESS);
while (pio_device->status==0) {};
pio_device->data=3;
, причем быстродействие опять-таки не пострадало, а даже и чуть выросло, поскольку компилятор держит указатель в регистре для и для второй команды его не загружает. Единственный недостаток этого метода — адреса регистров действительно дожны быть рядом, в идеале следовать вплотную, хотя пропуск можно организовать вставлением в структуру пустых полей. Другой недостаток — мы всемерно полагаемся на компилятор в плане упаковки наших полей в реальные адреса и должны четко представлять требования к выравниванию данных.
Что то многовато получилось по поводу адресации, поэтому работу с битовыми полями рассмотрим в части 2, если тема интересная.
Share post

Comments 15

    +3
    поэтому работу с битовыми полями рассмотрим в части 2, если тема интересная.

    Давайте рассмотрим, тема интересная.
      +3
      А как же volatile?
        0
        конечно volatile обязателен.
        Просто он у меня в макросе забит и я про него забыл.
          0
          Стоп. Const или volatile?
        0
        Вставлю свои пять копеек.
        Я не объявляю переменную, просто define, также добавляю volatile, чтобы компилятор не оптимизировал нашу переменную.

        typedef struct Uart_s {
          UInt irqEn;
          UInt irq;
          UInt addr;
          UInt data;
          UInt num;
        } Uart;
        
        #define UART ((volatile Uart *)UART_ADDR)
        


        Пример использования

        UART->irqEn = UART_IRQ_EN_TX;
        irq = UART->irq;
        
          0
          1. Вы забыли упомянуть __attribute__((packed)) и #pragma pack.
          2. Про недостаток этого способа: если есть reserved-дыры в наборе регистров устройства их придется заполнять и не дай бог вам ошибиться — поплывут все регистры, расположенные ниже.
          ИМХО: Запись и чтение без макросов/функций может затруднить процесс разработки — визуально проще контролировать необходимость сбоса кэшей, барьеры, а также вставлять отладку.
            0
            Вот так всегда у меня — сначала разъясняю то, что более-менее очевидно, а потом, увидев много букв, начинаю пропускать необходимые вещи. В последнем абзаце я на них намекнул, наверное надо было поподробнее.
            0
            оператор @ (и директива #pragma location)

            ТАК ВОТ ЧТО ЭТО ЗА ХРЕНЬ!
              0
              Хм?
                0
                Да я в свое время мозг себе вот этим повредил:
                // Register: PORTA
                volatile unsigned char PORTA @ 0x005;
                // bit and bitfield definitions
                volatile bit RA0 @ ((unsigned)&PORTA*8)+0;
              0
              А разве в студии нет *.h файлов для процессоров с #define всей периферии?
                +1
                … процессора?
                -Есть
                -А про что тогда статья?
                -Про дефайны *внешней* периферии!
                  0
                  Строго говоря про то как их лучше делать.
                  Хотя «лучше» в данном случае понятие весьма относительное и субъективное
                0
                Вчера перед сном пришло в голову, например для установки последовательно идущих регистров:

                int setRegister(int *base, char *fmt, ...)
                {
                    va_list st;
                    va_start(st,fmt);
                    
                    while (*fmt)
                    {
                        if (*fmt == '%')
                        {
                           fmt++; 
                           switch(*fmt)
                           {
                               case 'i':
                               {
                                   *base = va_arg(st,int);
                                   base++;
                                   fmt++;
                                   continue;
                                   
                               } break;
                                   
                               default:
                                   fprintf(stderr,"unknown option '%c' \n",*fmt);
                                   exit(EXIT_FAILURE);
                                   break;
                           }
                                       
                        }
                        
                        fmt++;
                    }
                
                    va_end(st); 
                }
                
                  0
                  и пример использования

                  setRegister(TIM1,«PERIOD: %i PREDDIV: %i TOV2: %i»,1001,1002,1003);

                  Only users with full accounts can post comments. Log in, please.