Что может быть хуже костылей? Только неполно документированные костыли.  Перед вами скриншот из последней официальной интегрированной среды разработки для 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. Если опустить часть про строки, то всё предельно просто. Рассмотрим пример. Для ОЗУ пишем так: А для ПЗУ так: Так просто, что эта технология неоднократно освещалась даже в рунете. Помните утверждение, что 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 бита. Эврика! Пишем код: Он компилируется, но не работает так, как нам бы этого хотелось (если array2d расположен после 32 КБ). Почему? Да потому, что операция & возвращает знаковое 16-битное число! Забавно, что семейство pgm_read_*_near принимает беззнаковые 16-битные адреса, то есть способно работать с 64 КБ данных, а операция & полезна лишь для 32 КБ. Идём дальше. Что у нас есть в pgmspace.h помимо pgm_read_*? Функция pgm_get_far_address(var), имеющая аж полстраницы описания, и заменяющая операцию &. Наверное, правильно так: Ошибка компиляции. Читаем описание: '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,... Ставим очередной костыль: переходим от индексов массивов к арифметике указателей: Вот теперь всё работает. Если вы пишете на Си/Си++ для 8-битных микроконтроллеров AVR, используя компилятор GCC, и храните данные в ПЗУ, то: