Хабрахабр

[Из песочницы] Преодоление порога 32 КБ для данных в ПЗУ микроконтроллеров AVR

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

image

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

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

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

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

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

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

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

Причём данные эти неизменяемые, известные на момент прошивки. Иногда в микроконтроллер нужно поместить не просто много, а очень много данных: столько, что они просто не помещаются в ОЗУ. В то же время код зачастую занимает лишь небольшую долю имеющегося ПЗУ. Например, растровая картинка, мелодия или какая-нибудь таблица. Легко! Так почему бы не использовать оставшееся место под данные? 0. В документации avr-libc 2. Если опустить часть про строки, то всё предельно просто. 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]));

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

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

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

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

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

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

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

Пишем код:

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

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

Что у нас есть в 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 Б.

Ссылки

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»