Ты помигал светодиодиком, собрал метеостанцию и робота на радиоуправлении, а потом успел разочароваться в этих маленьких кусочках кремния, что зовутся микроконтроллерами? Напрасно! Сегодня я покажу, как научить Arduino работать с внешней памятью и выжать из нее максимум для твоих проектов.

Сердцем мира Arduino всегда был и остается крохотный микроконтроллер ATmega328P. Однако сегодня его скромные характеристики (16 МГц, 2 Кбайт ОЗУ и 32 Кбайт ПЗУ) и устаревшая восьмибитная архитектура AVR уже становятся препятствием при работе с аудио, графикой и сетью. К счастью, за эти годы вокруг Arduino успело сложиться огромное сообщество любителей и разработчиков. Общими усилиями в проект была добавлена поддержка самых разных микроконтроллеров, в том числе очень популярное семейство STM32.

Их основой служит 32-разрядное процессорное ядро компании ARM. Параметры конкретной микросхемы зависят от модельной линейки (Value Line, Mainstream, Performance), но даже самые слабые модели уверенно обгоняют ATmega AVR в доступных возможностях и производительности. Если добавить сюда богатую периферию, многочисленные интерфейсы для связи с внешним миром и разумную цену, то совсем неудивительно, что эти микроконтроллеры полюбились сообществу и получили широкое распространение.

INFO

В компании ARM инженеры не лишены чувства юмора. Сегодня существуют три семейства процессорных ядер для встраиваемых систем — Cortex-A (application), Cortex-R (real-time) и Cortex-M (microcontroller). Индексы в названиях образуют имя архитектуры — ARM. Знакомые буквы можно найти и в сокращенном названии основного справочного документа — ARM Architecture Reference Manual (PDF). Такие вот шуточки для тех, кто в теме.

В предыдущих статьях («Как реализовать шифрование для самодельного гаджета», «Ищем энтропию на микросхеме, чтобы повысить стойкость шифров») я использовал плату Discovery с микроконтроллером F746NG. Это очень мощное решение на основе Cortex-M7, с частотой в 216 МГц, 320 Кбайт оперативной памяти и мегабайтом флеша. Но что делать, если вдруг и этого мало? К примеру, если попытаться работать с размещенным на этой же плате дисплеем с разрешением 480 на 272 пикселя, то при глубине цвета в 32 бита нам потребуется как минимум 510 Кбайт памяти, чтобы хранить буфер кадра.

Если при отрисовке дополнительно использовать буферизацию и рисовать в теневой кадр, пока предыдущий отображается на дисплее, то это значение нужно удвоить. Получившийся мегабайт, нужный для одной только графики, в оперативную память нашего микроконтроллера решительно не лезет. Что же делать?

К счастью, у F746NG есть периферийный блок FMC (Flexible Memory Control) для работы с интерфейсами внешней памяти. А на самой плате уже распаяна микросхема SDRAM на 128 Мбит (не байт!). Но почему-то при портировании в Arduino IDE авторы STM32duino не захотели добавлять ее поддержку в файлах для платы Discovery. Если сделать все грамотно и аккуратно, то после инициализации аппаратного уровня для нашей программы не будет никакой разницы, работать ли с внутренней памятью или с внешней. Я считаю, надо попробовать!

INFO

Сейчас порой кажется, что в эпоху AVR микроконтроллеры обладали только стандартным набором периферии и ничего сложного подцепить к ним было нельзя. Однако это не так — к некоторым из них тоже можно было подключить внешнюю память через интерфейс XMEM и отобразить ее в адресное пространство ЦПУ. Например, на МК ATmega8515 можно было увеличить ОЗУ со скромных 512 байт до внушительных 64 Кбайт! Тогда это, конечно, поражало воображение.

 

Подготовка

Итак, нашей главной целью будет размещение 8 Мбайт оперативной памяти в адресном пространстве процессора Cortex-M7. Да, я не ошибся — хоть у нас и 128 Мбит (16 Мбайт) во внешней микросхеме, но из-за того, что на плате разведена 16-битная шина данных (вместо максимально возможных 32 бит), нам по факту доступна только половина этого объема. Это, конечно, неприятно, но тут уж ничего не поделать.

Начинать следует с поиска документации и знакомства с ней. Для удобства я уже собрал все необходимое в сноске ниже. На первый взгляд там в руководствах сотни страниц и осилить их едва ли возможно, но на практике это вовсе и не требуется. Проверить адрес и описание регистров в одном месте, подсмотреть тайминги в другом, прочитать пару абзацев в третьем — вот, собственно, и все.

Посмотрим, как выглядит адресное пространство на нашем микроконтроллере. Это восемь блоков по 512 Мбайт (итого 4 Гбайт — максимум для 32-разрядных процессоров). Системное ОЗУ располагается в первом блоке и занимает адреса с 0x20000000 по 0x2004FFFF, предоставляя в наше распоряжение 320 Кбайт статической памяти. Больше можно получить только с помощью FMC и внешней микросхемы. FMC работает с адресами из блоков с третьего по шестой, но подключить SDRAM можно только в два последних.

Я предлагаю разместить внешнюю память в пятом блоке. Так мы получим дополнительные 8 Мбайт по адресам с 0xC0000000 по 0xC07FFFFF. Периферийный блок FMC будет обрабатывать все запросы на доступ к памяти в этой области и по параллельной шине переправлять в микросхему SDRAM. В целом такое взаимодействие между ЦПУ и внешним ОЗУ мало чем отличается от работы с памятью на любом компьютере, смартфоне или ноутбуке. Да, там все это гораздо быстрее и объемы существенно больше, но принципиальных отличий немного.

По существу, от нас требуется выполнить функции BIOS по инициализации аппаратной части и сделать ровно три вещи. Во-первых, сконфигурировать выводы микроконтроллера для работы с параллельным интерфейсом. Во-вторых, настроить FMC под нашу оперативную память (да, придется поиграться с таймингами). В-третьих, загрузить регистр с параметрами работы в саму микросхему. В итоге наша основная функция будет выглядеть как-то так:

void xmem_init() {
  pin_init();
  fmc_init();
  sdram_init();
}

Осталось только последовательно написать реализацию для каждого этапа и органично вставить это куда-нибудь в код скетча.

 

Интерфейс памяти

Прежде всего нужно определиться с физическим представлением нашего интерфейса памяти. У нас есть отдельные сигнальные линии для адресов (A[25:0]), данных (D[31:0]), команд (CAS, RAS, WE, CS) и тактирования (CLK, CKE). Дело несколько облегчается тем, что используется далеко не полный набор (про урезанную шину данных я уже упоминал), но и всего этого немало.

Схему подключения SDRAM к микроконтроллеру можно найти в документации на плату. Там же можно узнать, что для адресации используются 12 бит и еще два дополнительных бита указывают на один из четырех внутренних банков памяти в микросхеме. Для наглядности я свел все данные в небольшую табличку.


Теперь остается только сконфигурировать все выводы GPIO для работы в альтернативном режиме с FMC.

#include "stm32f746xx.h" /* for CMSIS defines */

static inline void pin_init() {
  /* Enable clock for GPIO ports */
  RCC -> AHB1ENR |= RCC_AHB1ENR_GPIOCEN | RCC_AHB1ENR_GPIODEN |
                    RCC_AHB1ENR_GPIOEEN | RCC_AHB1ENR_GPIOFEN |
                    RCC_AHB1ENR_GPIOGEN | RCC_AHB1ENR_GPIOHEN;

  /* Configure GPIO ports */
  /* PC3: AF PP + VHS + AF12 */
  GPIOC -> MODER   |= GPIO_MODER_MODER3_1;
  GPIOC -> OSPEEDR |= GPIO_OSPEEDER_OSPEEDR3_1 | GPIO_OSPEEDER_OSPEEDR3_0;
  GPIOC -> AFR[0]  |= GPIO_AFRL_AFRL3_3 | GPIO_AFRL_AFRL3_2;
  ...
}

Здесь мы последовательно подаем тактирование на порты GPIO (без этого никак), затем для PC3 задаем режим работы, устанавливаем максимальную скорость и выбираем его в качестве вывода FMC. Остается только повторить операции выше для всех остальных сигналов.

Это самая нудная и примитивная часть всего процесса. Хуже того, ошибаться тут никак нельзя, поскольку заметить опечатку потом будет очень сложно. Так что я выделил код для всех оставшихся выводов в отдельном листинге.

 

Настройка FMC

Следующим этапом нужно сконфигурировать блок FMC для нашей микросхемы памяти. Большую часть нужных параметров можно почерпнуть из даташита, но самое главное решение предстоит принимать самостоятельно. Конечно же, я говорю о тактовой частоте оперативной памяти. Именно она будет определять все остальные тайминги, и именно от нее будет зависеть итоговая производительность нашей системы.

По умолчанию FMC тактируется от частоты процессора — HCLK. Для F746NG максимальное значение составляет 216 МГц, и базовая настройка в скетче Arduino (до вызовов функций setup и loop) выставляет именно его. Конечно же, выбрать такую частоту для микросхемы памяти было бы слишком, ведь по документации она работает только вплоть до 167 МГц.

Поэтому блок FMC позволяет выбрать предделитель для частоты SDRAM. На выбор дается только два значения: /2 и /3. И это крайне неприятно, так как у нас есть дополнительное ограничение на максимальную частоту самого FMC и больше 100 МГц на нем выставлять вроде как нельзя. Но очень хочется!


Что же делать — снижать частоту ядра до 200 МГц или выбирать делитель /3 и довольствоваться скромными 72 МГц на оперативной памяти? Конечно же, это все неправильные варианты. Надо разгонять, и разгонять по максимуму! Окей, звучит несколько самоуверенно, но на самом деле у нас очень хорошие шансы на успех, и я постараюсь кратко объяснить почему.

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    2 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии