Разгон оперативной памяти на STM32 и Arduino

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

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

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

В компании 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. Если сделать все грамотно и аккуратно, то после инициализации аппаратного уровня для нашей программы не будет никакой разницы, работать ли с внутренней памятью или с внешней. Я считаю, стоит попробовать!

Сейчас порой кажется, что в эпоху 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 по инициализации аппаратной части и сделать ровно три вещи.

  1. Во-первых, сконфигурировать выводы микроконтроллера для работы с параллельным интерфейсом.
  2. Во-вторых, настроить FMC под нашу оперативную память (да, придется поиграться с таймингами).
  3. В-третьих, загрузить регистр с параметрами работы в саму микросхему.

В итоге наша основная функция будет выглядеть примерно так:

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

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

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

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

разогнать оперативную память STM32 Arduino

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

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

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

Настройка FMC

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

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

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

разогнать оперативную память Arduino

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

Работа любой микросхемы гарантируется ее производителем только в определенном диапазоне температур и напряжения питания (такую информацию всегда можно найти в даташите). Ключевое слово здесь — «гарантируется». Эти цифры — не уловки пиарщиков и маркетологов и эфемерные «+200% эффективности и скорости», которые замерили в идеальных условиях, а по факту в реальной жизни никто не увидит.

Нет, тут все серьезно. Перед тем как запустить микросхему в продажу, производитель проводит все необходимые тесты у себя в лабораториях, а главный инженер расписывается под результатами кровью. Ладно, про кровь я пошутил, но идею вы наверняка поняли.

У нас же почти «тепличные» условия использования для микросхемы — комнатная температура и стабильное напряжение на уровне 3,3 В. Как раз ровно посередине рекомендуемого диапазона 3,0–3,6 В. Так что все будет уверенно работать и на 108 МГц, еще и тайминги можно будет занизить.

РЕКОМЕНДУЕМ:
Espruino Pico: програмирование USB-микроконтроллера на JavaScript

Кстати, о таймингах. Теперь, когда известна тактовая частота, можно подсчитать длительность периода. Это примерно 9 нс, большая точность нам не требуется (несмотря на то что в секундах это цифра крохотная и в ней прилично нулей после запятой). И да, обратите внимание, у нас SDR (Single Data Rate) память, а не более привычная по миру персональных компьютеров DDR (Double Data Rate). Это значит, что синхронизация происходит только по одному фронту тактового сигнала вместо двух. Грубо говоря, в два раза медленнее при той же частоте.

Зная длительность периода, теперь легко вычислить величину всех нужных таймингов (для этого посмотрите таблицы 12 и 13 в даташите на микросхему памяти). Там, где их значение указывается в наносекундах, нужно подобрать такое количество тактов, чтобы их суммарная длительность была бы больше. Как пример — значение TXSR в таблице составляет 70 нс, а значит, надо указывать для него задержку в восемь тактов ( 8 х 9 = 72 > 70).

В общем, это не самая простая часть всей настройки, но в коде все выглядит вполне компактно (всего три записи в регистры).

Мы потратили немало времени, листая даташит на память и выясняя, с какими настройками ее нужно запускать. Но откуда эту информацию берет обычный персональный компьютер, ведь там планки памяти — это практически plug & play?

Оказывается, помимо нескольких микросхем оперативной памяти, на каждой планке дополнительно присутствует энергонезависимая SPD EEPROM. Именно она хранит значения частоты и таймингов и сообщает их системе по последовательному интерфейсу SMBus (аналог I2C).

В функции fmc_init значения таймингов гарантированно рабочие и потому несколько завышены. Забегая вперед, скажу, что у меня получалось запустить плату с TRAS=4 и TXSR=7 без каких-либо ошибок. От знакомых слышал, что и это не предел и разгонять можно и дальше. Но уже разве что из спортивного интереса — особого прироста тут не получить.

Запускаем SDRAM

Предыдущие две функции настраивали периферию только на самом контроллере. Теперь пришло время «пробудить» микросхему SDRAM и послать ей стартовые команды. Весь процесс детально описан на странице 35 документации на оперативную память. В коде это выглядит следующим образом:

Блок FMC выкинет на шину памяти несколько команд, среди которых нас больше всего интересуют ровно две: загрузка регистра режима (Mode Register) и таймера регенерации (Refresh Timer). Регистр режима выглядит так.

разогнать оперативную память stm32

Большинство комбинаций битов тут зарезервировано (очень интересно, зачем), а оставшиеся вполне можно оставить по умолчанию. Главное — не забыть выставить параметр CAS Latency, который определяет количество тактов между отправкой команды на чтение и появлением данных на шине.

Также следует указать, как часто данные в SDRAM нужно обновлять, ведь ячейки динамической памяти построены на основе конденсаторов и имеют свойство разряжаться со временем. Для этого сверимся с документацией на FMC (с. 379) и воспользуемся калькулятором.

разогнать оперативную память stm32

Все, самое сложное теперь позади! Достаточно только вызывать функцию xmem_init в setup и настроить SDRAM. С нашей микросхемой уже можно работать, и сейчас лучший момент проверить это, записав что-нибудь в память по новым адресам. Если не случилось никакого Bus Error (с экстраполяцией до Hard Fault), то примите мои заслуженные поздравления.

Правим ldscript

Уже на этом этапе мы можем работать с внешней памятью примерно таким образом:

Это кусочек кода с буферами ввода-вывода для библиотеки bearSSL из моей предыдущей статьи. Так как операции с массивами — это частные случаи арифметики указателей, все реализуется достаточно просто. Однако такое использование памяти чревато проблемами — стоит только опечататься в адресах или размерах массивов, и следующие несколько часов придется потратить на исправление глупой ошибки.

Облегчить себе жизнь можно, если добавить в программу менеджер памяти. Или взять существующий. Вы наверняка уже догадались, что дальше речь пойдет о библиотечной функции malloc. Именно она возьмет на себя всю низкоуровневую работу с памятью и адресами, позволив нам сконцентрироваться на более важных вещах.

Изначально куча размещается в той же области внутренней SRAM, что и стек. Если получится перекинуть кучу во внешнюю память, то malloc начнет раздавать адреса оттуда. Это удобно, так как, во-первых, у нас освободится больше места под стек, во-вторых, менеджер памяти сможет нарезать нам кусочки целыми мегабайтами.

Для этого нам нужно добавить новую область памяти в скрипт загрузчика. В файлах Arduino по адресу packages/STM32/hardware/stm32/1.5.0/variants/DISCO_746NG ищем ldscript.ld и открываем его в любом текстовом редакторе (исходную версию предварительно все же лучше сохранить). Перед секциями там указаны типы памяти в нашей системе. Исправляем на:

После этого ищем описание секции user_heap_stack. Оно выглядит как-то так:

Здесь происходит выравнивание данных по адресам в памяти и экспорт нескольких дополнительных переменных для компилятора. Это все уже не требуется, поэтому удаляем секцию полностью и пишем следующее:

Теперь куча будет размещаться во внешней микросхеме памяти, но нам еще нужно научить функцию malloc работать с новыми адресами. Править ее исходники было бы несколько опрометчиво (вот здесь вы сможете оценить примерный масштаб проблемы), да это и не требуется. Мы зайдем с другой стороны и обратимся к функции sbrk, от которой зависит malloc.

Реализуем sbrk

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

Прежде всего стоит взглянуть на файл syscalls.c, который можно найти по адресу packages/STM32/hardware/stm32/1.5.0/cores/arduino. Тут находится реализация sbrk по умолчанию, от нее и будем отталкиваться. Атрибут weak намекает, что в своих проектах эту функцию можно переопределять без последствий.

Создаем скетч в Arduino и добавляем новый файл sbrk.c. Пишем внутри:

Теперь функция malloc из стандартной библиотеки будет зависеть уже от нашей реализации системного вызова. А мы сделали все для того, чтобы он раздавал адреса из нужного диапазона [0xC0000000, 0xC07FFFFF]. Последнее неудобство заключается в том, что в каждой нашей программе, которая использует внешнюю SDRAM, нам необходимо вызывать xmem_init в setup перед динамическим распределением памяти. Иначе сказка закончится и карета превратится в тыкву значительно раньше полуночи. В смысле malloc вернет нулевой указатель, разумеется.

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

Добавляем в main

Проблема заключается в том, что функция main у нас одна для всех плат из пакета STM32duino, а Discovery F746 со своей внешней SDRAM в этом плане уникальна. Поэтому придется вводить дополнительные условия при компиляции скетча.

Снова идем по адресу packages/STM32/hardware/stm32/1.5.0/variants/DISCO_746NG (в этой папке мы уже правили скрипт загрузчика). Открываем заголовочный файл платы variant.h и добавляем в любое место строчку с новым определением.

Далее переходим в папку packages/STM32/hardware/stm32/1.5.0/cores/arduino. Ищем main.cpp, в него предстоит внести заключительные изменения, чтобы все заработало. Во-первых, добавляем файл xmem.h с объявлением функции инициализации:

И вставляем ее вызов прямо перед setup:

Кстати, если вы обратите внимание на остальной код в main, то заметите, что конфигурация интерфейса USB на некоторых платах устроена по такому же принципу.

Заключение

В этой статье мы научили Arduino работать с внешней памятью для платы на STM32. Дополнительные несколько мегабайт придутся очень кстати, если вы захотите использовать дисплей или подключить камеру.

РЕКОМЕНДУЕМ:
Как создать защищенное зашифрованное устройство

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

Понравилась статья? Поделиться с друзьями:
Добавить комментарий