Преодоление порога 32 КБ для данных в ПЗУ микроконтроллеров AVR

в 9:21, , рубрики: arduino, atmel studio, avr, C, c++, gcc, архитектура компьютера, микроконтроллеры, программирование микроконтроллеров

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

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 Б.

Ссылки

Автор: Caesarion

Источник

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


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js