Что может быть хуже костылей? Только неполно документированные костыли.


image


Перед вами скриншот из последней официальной интегрированной среды разработки для 8-битных микроконтроллеров AVR, Atmel Studio 7, язык программирования Си. Как видно из столбца Value, переменная my_array содержит число 0x8089. Другими словами, массив my_array располагается в памяти, начиная с адреса 0x8089.


В то же время столбец Type даёт нам несколько иную информацию: my_array является массивом из 4 элементов типа int16_t, расположенным в ПЗУ (это обозначается словом prog, в отличие от data для ОЗУ), начиная с адреса 0x18089. Стоп, но ведь 0x8089 != 0x18089. Какой же на самом деле адрес у массива?


Язык Си и гарвардская архитектура


8-битные микроконтроллеры AVR производства ранее Atmel, а ныне Microchip, популярные, в частности, из-за того, что они лежат в основе Arduino, построены по гарвардской архитектуре, то есть код и данные расположены в разных адресных пространствах. Официальная документация содержит примеры кода на двух языках: ассемблере и Си. Ранее производитель предлагал бесплатную интегрированную среду разработки, поддерживающую только ассемблер. А как же те, кто хотел бы программировать на Си, а то и Си++? Существовали платные решения, например, IAR AVR и CodeVisionAVR. Лично я им никогда не пользовался, ведь, когда я начал программировать AVR в 2008-м году, уже был бесплатный WinAVR с возможностью интеграции с AVR Studio 4, а в нынешнюю Atmel Studio 7 он просто включён.


Проект WinAVR основан на компиляторе GNU GCC, который разрабатывался для архитектуры фон Неймана, подразумевающей единое адресное пространство для кода и данных. При адаптации GCC к AVR был применён следующий костыль: под код (ПЗУ, flash) отводятся адреса с 0 по 0x007fffff, а под данные (ОЗУ, SRAM) — с 0x00800100 по 0x0080ffff. Были и всякие другие хитрости, например, адреса с 0x00800000 по 0x008000ff представляли регистры, к которым можно обращаться теми же опкодами, что и к ОЗУ. В принципе, если вы простой программист, наподобие начинающего ардуинщика, а не хакер, смешивающий в одной прошивке ассемблер и Си/Си++, вам не нужно всё это знать.


Помимо собственно компилятора WinAVR включает различные библиотеки (часть стандартной библиотеки языка Си и специфичные для AVR модули) в виде проекта AVR Libc. Последняя версия, 2.0.0, выпущена почти три года назад, а документация доступна не только на сайте самого проекта, но и на сайте производителя микроконтроллеров. Есть и неофициальные русские переводы.


Данные в адресном пространстве кода


Иногда в микроконтроллер нужно поместить не просто много, а очень много данных: столько, что они просто не помещаются в ОЗУ. Причём данные эти неизменяемые, известные на момент прошивки. Например, растровая картинка, мелодия или какая-нибудь таблица. В то же время код зачастую занимает лишь небольшую долю имеющегося ПЗУ. Так почему бы не использовать оставшееся место под данные? Легко! В документации avr-libc 2.0.0 этому посвящена целая глава 5 Data in Program Space. Если опустить часть про строки, то всё предельно просто. Рассмотрим пример. Для ОЗУ пишем так:


unsigned char array2d[2][3] = {...};
unsigned char element = array2d[i][j];

А для ПЗУ так:


#include <avr/pgmspace.h>
const unsigned char array2d[2][3] PROGMEM = {...};
unsigned char element = pgm_read_byte(&(array2d[i][j]));

Так просто, что эта технология неоднократно освещалась даже в рунете.


Так в чём же проблема?


Помните утверждение, что 640 КБ хватит каждому? Помните, как переходили от 16-битной архитектуры к 32-битной, а от 32-битной к 64-битной? Как Windows 98 нестабильно работала на более 512 МБ ОЗУ при том, что её разрабатывали для 2 ГБ? Случалось ли вам обновлять БИОС, чтобы материнская плата работала с жёсткими дисками более 8 ГБ? Помните джамперы на 80-ГБ жёстких дисках, урезающие их объём до 32 ГБ?


Первая проблема настигла меня тогда, когда я попытался создать в ПЗУ массив размером не менее 32 КБ. Почему именно в ПЗУ, а не в ОЗУ? Потому что в настоящее время 8-битных AVR с ОЗУ более 32 КБ просто не существует. А с более 256 Б — существуют. Вероятно, именно поэтому создатели компилятора выбрали для указателей в ОЗУ (и заодно для типа int) размер 16 б (2 Б), о чём можно узнать из чтения абзаца Data types, расположенного в главе 11.14 What registers are used by the C compiler? документации AVR Libc. Ох, а ведь мы не собирались хакерствовать, а тут регистры… Но вернёмся к массиву. Оказалось, что нельзя создать объект размером более 32 767 Б (2^(16 — 1) — 1 Б). Я не знаю, зачем длину объекта понадобилось делать знаковой, но это факт: никакой объект, даже многомерный массив, не может иметь длину 32 768 Б или больше. Немного напоминает ограничение на адресное пространство 32-битных приложений (4 ГБ) в 64-битной ОС, не правда ли?


Насколько я знаю, эта проблема не имеет решения. Если вы хотите поместить в ПЗУ объект длиной от 32 768 — дробите его на более мелкие объекты.


Ещё раз обратимся к абзацу Data types: pointers are 16 bits. Применим это знание к главе 5 Data in Program Space. Нет, теорией тут не обойтись, нужна практика. Я написал тестовую программу, запустил отладчик (к сожалению, программный, а не аппаратный) и увидел, что функция pgm_read_byte способна возвратить только те данные, чьи адреса укладываются в 16 бит (64 КБ; спасибо, что не 15). Потом происходит переполнение, старшая часть отбрасывается. Логично, учитывая, что указатели 16-битные. Но возникает два вопроса: почему об этом не написано в главе 5 (вопрос риторический, но именно он побудил меня написать эту статью) и как всё-таки преодолеть границу в 64 КБ ПЗУ, не переходя на ассемблер.


К счастью, помимо главы 5 есть ещё 25.18 pgmspace.h File Reference, откуда мы узнаём, что семейство функций pgm_read_* — это лишь переобозначение для pgm_read_*_near, принимающих 16-битные адреса, а есть ещё pgm_read_*_far, и туда можно подать адрес длиной 32 бита. Эврика!


Пишем код:


unsigned char element = pgm_read_byte_far(&(array2d[i][j]));

Он компилируется, но не работает так, как нам бы этого хотелось (если array2d расположен после 32 КБ). Почему? Да потому, что операция & возвращает знаковое 16-битное число! Забавно, что семейство pgm_read_*_near принимает беззнаковые 16-битные адреса, то есть способно работать с 64 КБ данных, а операция & полезна лишь для 32 КБ.


Идём дальше. Что у нас есть в pgmspace.h помимо pgm_read_*? Функция pgm_get_far_address(var), имеющая аж полстраницы описания, и заменяющая операцию &.


Наверное, правильно так:


unsigned char element = pgm_read_byte_far(pgm_get_far_address(array2d[i][j]));

Ошибка компиляции. Читаем описание: 'var' has to be resolved at linking time as an existing symbol, i.e, a simple type variable name, an array name (not an indexed element of the array, if the index is a constant the compiler does not complain but fails to get the address if optimization is enabled), a struct name or a struct field name, a function identifier, a linker defined identifier,...


Ставим очередной костыль: переходим от индексов массивов к арифметике указателей:


unsigned char element = pgm_read_byte_far(pgm_get_far_address(array2d) + i*3*sizeof(unsigned char) + j*sizeof(unsigned char));

Вот теперь всё работает.


Выводы


Если вы пишете на Си/Си++ для 8-битных микроконтроллеров AVR, используя компилятор GCC, и храните данные в ПЗУ, то:


  • при объёме ПЗУ не более 32 КБ вы не столкнётесь с проблемами, прочитав лишь главу 5 Data in Program Space;
  • при объёме ПЗУ более 32 КБ следует использовать семейство функций pgm_read_*_far, функцию pgm_get_far_address вместо &, арифметику указателей вместо индексов массивов, а размер любого объекта не может превышать 32 767 Б.

Ссылки


Комментарии (20)


  1. a-tk
    21.01.2019 13:07

    Интересно, а стандартные функции и библиотеки Arduino умеют пользоваться данными, размещёнными за пределами первых 32к? Например, snprintf_P и перегрузка Stream.print(const __FlashStringHelper*)


  1. Afterk
    21.01.2019 13:17

    Пробовали offsetof (костыль получше)?

    typedef struct {
        unsigned char array2d[2][3];
    } x_t;
    ...
    const x_t  v PROGMEM = {...};
    ...
    unsigned char a = pgm_read_byte_far(pgm_get_far_address(v) + offsetof(x_t, array2d[i][j]));
    

    Надо переходить на ARM, где единное 32 битное адресное пространство (если не перешли).


    1. JerleShannara
      21.01.2019 19:47

      Вот именно, я не вижу смысла в AVR, кроме как в формате восьминогих тараканов типа ATTiny, дешевле и проще поставить STM32F0/F1, или L серию, если надо ещё и по питанию экономию полную сделать. Есть конечно запущенные случаи, где сигнальные уровни 5V, но там можно и преобразователей уровня поставить.


      1. Armleo
        22.01.2019 15:08

        Или не ставить ибо у stm32 большинство ног 5v tolerant.


  1. andreili
    21.01.2019 13:34

    Вот из-за таких костылей в коде я и пересел на STM32 — никаких раздельных адресных пространств, все указатели 32 бита…


  1. AndyKorg
    21.01.2019 13:52

    Возможно появление массивов больше 32К в 8-битном решении намекает на необходимость использования для проекта более мощного МК?


    1. nafikovr
      22.01.2019 12:34

      что значит мощного?


      1. AndyKorg
        22.01.2019 13:12

        Обработка такого большого массива на 8-и битном МК возможно (зависит от Т.З.) занимает много времени. Поэтому возможно имеет смысл посмотреть в сторону увеличения разрядности.


        1. nafikovr
          22.01.2019 13:46

          а еще это может быть развитая менюшка, или набор констант читаемых раз в жизни, или… много всяких или. было у меня семейство приборов на сопровождении. мега2560 с почти полной пзу, а производительности хватало.


  1. Bratak
    21.01.2019 22:14

    Что же хотел сказать автор-вообще не понятно.Во-первых,8-битных авр с более чем 32 кб пзу предостаточно, это старшие модели атмеги, например 2560.Во-вторых, создатели компилятора ничего не выбирали просто так: пзу в авр адресуется словами по 2 байта, и также все указатели в авр в принципе 2-х байтные, поскольку это пары 8-битных регистров.В третьих, то-что вы называете функциями, типа pgm_read_byte, на самом деле макросы.Ну а самое интересное в том, что адресация при чтении из пзу осуществляется с помощью регистра z, поэтому адресуется 2^16-1 байт, поскольку один бит в этом регистре отвечает за выбор байта в слове.А вот адресация выше 32к — это уже ни разу не атомарная операция ELPM, поэтому лучше вместо того чтобы использовать не глядя кучу макросов из pgmspace.h, написать свой, короткий и простой.


    1. Caesarion Автор
      22.01.2019 11:16

      Что вы подразумеваете под «ни разу не атомарной операцией»? Что LPM, что ELPM выполняются 3 такта. Как вы перед этим заполняете регистры — другой вопрос. И почему бы вам не привести пример другого, короткого и простого макроса?


      1. Bratak
        22.01.2019 13:24

        Я макросами стараюсь не пользоваться, там просто достаточно отключить прерывания, положить смещение в рампз, и читать откуда нужно.А у вас опять куча воды про 32 х битную адресацию на меге-зачем вообще.И вся проблема в том что нету никакой проблемы-читайте мануалы, их нужно то всего да, и на ломайте головоу там где этого не нужно.


        1. Caesarion Автор
          22.01.2019 13:35

          Вся эта статья возникла из-за чтения мануалов. Читаю главу 5, делаю как написано. До 32 КБ работает, потом нет. Вот что я, по-вашему, должен был делать в таком случае? Какие именно мануалы читать, если не те, что я и читал? Как не думать о 32-битной адресации, если нужные данные лежать за пределами 16-битного адресного пространства? При чём тут вообще прерывания? Откуда я мог взять смещение, которое нужно положить в «рампз»?


          1. Bratak
            22.01.2019 13:43

            Мануалы подразумеваются датащит на мк и avr instruction set, библиотеки си для авр написаны сообществом и могут таить в себе недокументированные баги.Если ты работаешь с микроконтроллером, гораздо проще написать кусок на асме, чем использовать чьи-то библы и ругаться потом на них.Прерывания при том, что они могут вызвать обработчики, которые изменять регистровую пару z и будут веселье.Чтобы получить смещение надо взять адрес того, что ты читаешь из пзу.Ничего сложного.


            1. Caesarion Автор
              22.01.2019 13:53

              Вот теперь ваша мысль ясна. Я же в самом начале статьи написал, какому именно компилятору она посвящена. Ваша ветвь комментариев — точно такой же оффтоп, как и те, где рекомендуют сменить МК.


      1. DolphinSoft
        23.01.2019 09:01

        «ни разу не атомарная операция», означает в данном контексте, что для получения адреса за пределами смещения в 32к (ограниченных 15 битами регистровой пары Z), необходимо изменять RAMPZ, потому что адресация ELPM использует пару RAMPZ:Z


  1. ukt
    22.01.2019 00:11

    Аппаратные регистровые указатели у AVR — двухбайтные, X, Y, Z.


    1. Caesarion Автор
      22.01.2019 11:12

      Об этом и речь. До тех пор, пока вам достаточно 2 Б (в ассемблере это регистр Z и инструкция LPM — Load Program Memory), всё хорошо. Чтобы выйти за 64 КБ (а по факту за 32), приходится использовать 3-Б аппаратные регистры, а именно RAMPZ:Z и инструкцию ELPM (Extended Load Program Memory). И вся проблема в том, что в главе 5 мануала AVR Libc об этом ни слова, хотя сам модуль pgmspace содержит всё нужное.


      1. nafikovr
        22.01.2019 12:40

        Об этом и речь.

        речь о том что работать с 32 битными адресами при наличии только 16 битных указателей пришлось бы с гораздо большим количеством костылей. при чем всегда, а не только когда требуется выйти за пределы 32 кбайт.

        хотя большие аврки уже сами по себе на костылях стоят.


  1. Uzix
    22.01.2019 17:28

    Посмотрите gcc.gnu.org/onlinedocs/gcc/Named-Address-Spaces.html, спецификатор __memx. Мне им не приходилось пользоваться, но, похоже, это то, что Вам нужно.