Что может быть хуже костылей? Только неполно документированные костыли.
Перед вами скриншот из последней официальной интегрированной среды разработки для 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 Б.
Ссылки
- Microchip — производитель микроконтроллеров AVR и разработчик IDE Atmel Studio
- AVR Libc Home Page
- AVR. Учебный Курс. Программирование на Си. Работа с памятью, адреса и указатели — DI HALT о больших адресах не упомянул в принципе, а где-то глубоко в комментариях написали о
pgm_get_far_address
, но образец дали неработающий