
-- Текст написан без AI
Зачем нужен eMMC-эмулятор?
eMMC накопители – распространенный вариант энергонезависимого хранения данных во встраиваемых и портативных устройствах. Вы можете найти микросхемы, работающие на этом стандарте, там, где скорости SPI флешки недостаточно, а SATA SSD избыточен/слишком дорогой.
Зачем может пригодиться эмулятор eMMC? Он может понадобиться как для рутинных задач, вроде проверки своего софта на совместимость с накопителем, так и для задач в области безопасности – исследования работы чужого софта с накопителями, которые ведут себя нестандартным образом.
В этом смысле eMMC-стандарт был удивительным образом обделен эмуляционными решениями. Причем заметьте, какие-то полумеры существуют: например, старые версии спецификации eMMC допускают возможность, используя 1х ширину шины и минимальную частоту, войти в legacy SPI режим, и воспользоваться эмулятором SPI, но его поддержка была формально убрана в стандарте eMMC версии 4.3 (сейчас со стороны производителей активно внедряется стандарт 5.1, но большинство хостов пока ориентируются максимум на 5.0). Вдобавок, многие хосты требуют свой набор поддерживаемых функций – ограничивают карты не только «сверху», но и «снизу», не давая им работать в низкоскоростном legacy MMC режиме. Таким образом, даже если бы существовал MMC эмулятор, он бы всё равно не везде подошел: например, при работе с vendor-specific форками U-Boot (которые, как было установлено в экспериментах на отдельных платах, показанных далее, «отказывают» картам без High-Speed режимов).
В итоге мы пришли к тому, что нам нужен полнофункциональный eMMC эмулятор или, что реалистичнее, такой эмулятор, который возможно доработать до нужных каждому хост-контроллеру функций.
Выбор инструмента

Иными словами, нужно сделать настоящий eMMC эмулятор, способный, при необходимости, быть допиленным до набора функций из стандарта версии 5.1, чтобы иметь возможность работать с большинством хост-контроллеров eMMC. Какие варианты, альтернативные ПЛИС-эмуляции, можно рассмотреть:
MITM между хостом и настоящей картой eMMC. Перехватывать только те диалоги между хостом и картой, которые нам необходимо перехватить, и модифицировать именно их. Однако, мало того, что это решает только часть задач, на которые способен настоящий эмулятор, специфика протокола eMMC делает эту задачу очень трудоёмкой, возможно даже нереализуемой для большинства скоростных режимов. Проблема в том, что устройство имеет 9 input-output линий (1 командная, 8 линий данных), и достаточно строгие тайминги ответов на запросы (особенно по командной линии). Это уменьшает вероятность того, что MITM-устройство успеет распознать входящую команду, отправить измененную команду в устройство, а подходящий ответ – на хост в срок (естественно, «заблокировав» ответ на «поддельную» команду от карты).
Софтовая эмуляция. По сути, исключена требованиями по скорости. Мы говорим о скоростях от 400 кГц (где это еще возможно) до 200 МГц (в режиме DDR), где bitbang, обычно ограниченный частотами в 1-2 МГц, не справится ни при каких обстоятельствах.
Эмуляция при помощи PIO на Raspberry Pi Pico. Крайне затруднительна с учетом имеющихся требований по скорости и функциональности ввиду того, что лимит в 32 инструкции на стейт-машину недостаточен для реализации полного дерева условий eMMC.
Большая часть современных ПЛИС теоретически дает нам все возможности для реализации eMMC протокола:
Заявленная способность разогнаться до 400 МГц (по крайней мере, на регистрах и LUT);
Встроенные примитивы IOBUF на всех GPIO-банках, позволяющие вести работу с двунаправленной шиной;
Наличие clock-capable пинов для подключения мастер-клока хост-контроллера;
Обширные ресурсы для реализации конечного автомата eMMC карты;
Но это всё достаточно распространенный функционал. Однако есть то, что заставило в итоге остановиться на AMD Kintex Ultrascale XCKU060:
Наличие GPIO-банков, дающих широкий диапазон поддерживаемых напряжений, позволяющий работать на 3.3В, 1.8В, 1.2В – всех напряжениях, которые обычная микросхема eMMC может поддерживать.
Мощный контроллер DDR памяти, способный подключать объем памяти как минимум выше 2ГБ (требование спецификации для eMMC устройств).
Она была под рукой в лаборатории :)

Пояснительная бригада
Протокол XVC, применяемый для удаленной отладки стенда с ПЛИС в инструментах разработки Xilinx, обладает неприятной особенностью отсутствия keep alive на сетевом соединении, поэтому выключается достаточно часто (сбрасывая данные в окнах ILA, и тд).
Конкретно в картинке – следующая из этого обстоятельства забавная, но неприятная особенность – Vivado кидает сообщения об постоянном прерывании соединения во все логи и сообщения, перемешивая их с ошибками и успехами сборки.
Разработка архитектуры
«Месяц работы над кодом может сэкономить час продумывания архитектуры»
Электрический стандарт eMMC дает вполне понятный формат передачи данных, имеющий схожие моменты с другими протоколами полудуплексной передачи данных. В голову сразу пришла аналогия с I2C и его поочередным перехватом канала передачи данных, синхронизированных по мастер-клоку:

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

Логически обработка команд и данных также поделена на два крупных блока: командный модуль, отвечающий за прием и передачу коротких команд, и модуль данных, занимающийся приемом и передачей данных по результатам работы командного модуля.
То есть иерархия следующая:
Хост передает некую команду по emmc_cmd линии, после чего переводит её в высокоимпедансное состояние;
Карта принимает эту команду, меняет внутренние состояния на основе неё и посылает ответ по установленному спецификацией шаблону на той же emmc_cmd линии, после чего опять переводит её в высокоимпедансное состояние;
Далее, если команда подразумевает передачу данных, задействуется блок данных по аналогичной схеме. В случае записи – «хост передает данные -> карта подтверждает запись», в случае чтения – «карта передает данные -> хост подтверждает прием».
Это все, разумеется, общие положения. Но их достаточно, чтобы понять внутреннюю архитектуру RTL-блока, который нам понадобится:

Наверное, первое что бросится в глаза – целая куча респондеров и один объединенный модуль обработки данных (еще и двунаправленный!). Тому есть причина – несмотря на довольно большое количество команд (причем некоторые из них в группе application-specific – то есть об их назначении в конкретных хостах остается лишь гадать), все форматы и длины ответов на них регламентированы в спецификации. Все они отправляют разные комбинации внутренних переменных, поэтому вынесение их в отдельные блоки имеет смысл.
Второе, что может броситься в глаза – разные механизмы подсчета CRC – для данных и для команд контрольная сумма считается по разному. Независимо от длины команды/ответа на команду/блок данных, к каждой линии жестко привязан свой тип генератора, который применяется как для проверки входящих сообщений (в задачи карты входит отбраковка команд/данных с битым CRC), так и для отправляемых (хост также проверяет ответы/данные со своей стороны).
Итак, что нам дает спецификация:
CRC7. Надо полагать, сделан в таком необычном размере для того, чтобы быть совместимым с еще MMC-шными командами и ответами размером ровно в 48 бит (сорок седьмой с нуля бит сразу после CRC7 занят стоп-битом). Правда, к 136-битным ответам он тоже применяется. Использует полином 0x9. Был отлажен в симуляции с помощью этого калькулятора, после чего хорошо показал себя в железе.
CRC16. Обладает интересной спецификой – считается для каждой линии данных в отдельности (то есть его длина на шине всегда 16 тактов, независимо от ширины шины). Поэтому я заложил возможность инстанцировать несколько таких блоков. Считается с полиномом 0x1051. Удобно проверить при помощи вот этого калькулятора.
Следующий вопрос важнее всех предыдущих – как это тактировать. По сути, у нас два варианта:
Внутреннее тактирование всей логики работы блока вплоть до буферов к IOBUF, со сбором и выдачей входящих и исходящих данных/команд при помощи асинхронных FIFO между доменом тактирования emmc_clk и fpga_clk.
Внешнее тактирование с помощью внешнего хост-клока emmc_clk абсолютно всего, что это выдержит (то есть всего, кроме контроллера DDR, управляющего софт-процессора и бриджа для инициализации оперативной памяти через ПЛИС – для этого тоже потребуются асинхронные FIFO).
То есть, проще говоря, вопрос сводится к тому, где разграничивать clock-домены асинхронными FIFO. Первый подход грозит большим временем отклика (особенно с повышением частоты хост-клока относительно внутренней частоты ПЛИС), второй - трудностями на этапе трассировки дизайна (особенно с timing analysis – невозможно корректно посчитать задержки для частоты, которую среда не знает).
Ниже графически изображены описанные варианты тактирования.

В итоге, второй вариант был выбран как более удобный для реализации - FIFO между контроллером ОЗУ и eMMC блоком в любом случае будет необходим, почему бы не совместить неприятное с бесполезным и не сделать их точкой пересечения доменов тактирования.
Последнее, на чем хотелось бы остановить внимание - включенные в модуль eMMC эмулятора блоки BRAM Memory Generator. С fs_sim всё понятно - это просто образец будущей памяти, на который можно залить какой-нибудь заголовок файловой системы и посмотреть как её читают (больше из-за ограничений ПЛИС на BRAM-примитивы не влезет). Второй блок предназначен для EXT_CSD – особого регистрового пространства карты eMMC, в котором содержится расширенная информация о её функционале (как продолжение устаревшего, но компактного CSD регистра прямиком из легаси стандарта MMC).
Итак, у нас есть модуль. С какой программы тестов стоит начать, под какой сценарий писать первый тестбенч?
Набор функций для минимального рабочего прототипа
Найти этот набор не так-то легко. Идея в том, что несмотря на то, что я делаю slave-устройство, у него все еще есть возможность ограничивать свои режимы работы, как раз-таки при помощи вышеупомянутых регистров, включая EXT_CSD. И в моих интересах в начале разработки уменьшить число этих режимов как можно сильнее, чтобы не реализовывать с порога поддержку всех высокоскоростных режимов.
По сути, схема всех возможных режимов eMMC выглядит как-то так:

Каждая комбинация ширины + скорости, теоретически, является отдельным случаем, который следует отладить. Если есть вариант заблокировать использование всех скоростных режимов кроме самого медленного, стоит им воспользоваться.
Чтобы понять, что скрывается за загадочным «legacy MMC» режимом, обратимся к истории спецификаций:

Понятно, что хост, в зависимости от своей новизны и встроенных возможностей, может не поддерживать какие-то версии протоколов SD/eMMC. Но в конечном счете, все хосты обязаны исполнять в каком-то виде следующий цикл идентификации:
Определение принадлежности карты *MMC или SD стандарту через ответы на команды CMD1/CMD55;
Если *MMC – определить, поддерживает ли она функции eMMC через MMC-шный CSD;
Если eMMC – определить, какой максимальный скоростной режим поддерживается.
При этом, для всей инициализации и идентификации используется нижняя частота MMC – как раз те ~400 кГц:
Это позволяет активно читать происходящее на шине как интегрированным в ПЛИС, так и внешним логическим анализатором. Практика показала, что имеет смысл применять оба, так как:
Интегрированный логический анализатор (ILA) позволяет «залезть» во внутренние состояния синтезированного блока;
Внешний логический анализатор позволяет записать весь обмен на шине для дальнейшей дешифровки (в отличии от ILA, где небольшая глубина буфера это не позволяет).
В итоге план расширения поддерживаемых режимов выглядит следующим образом:
Реализация режима Legacy MMC для прохождения инициализации.
Добавление поддержки одного High Speed режима с минимальной частотой, которая поддерживается тестовым хостом.
Реализация остальных High Speed режимов.
Выбор тестового таргета и хоста
Выше часто упоминается спецификация eMMC, что намекает на ее доступность и открытость. Но как бы не так. eMMC стандарт имеет определенную специфику, которая превращает разработку в что-то, что я не побоюсь назвать реверс-инжиниринговым исследованием. Вкратце, факторы следующие:
Спецификации закрытые, а те что доступны для скачивания старые и зачастую неполные. Документация последней версии стандарта 5.1 распространяется JEDEC на платной основе. Специфику работы фич, введенных в этой версии (а на минуточку, там ввели еще один провод в шину – data strobe) приходится раскапывать по косвенным свидетельствам.
Еще большую путаницу добавляет то, что многие пункты спецификации не соблюдаются так строго, как я полагал изначально. Оказалось, ничего не мешает производителям карт сделать чуть менее жесткие проверки для хостов при переключении режимов и состояний.
Вдобавок, как я уже упоминал выше, хост-контроллеры могут отсекать какие-то режимы по своим причинам (например, из-за слишком низкой скорости); поэтому надо попытаться найти такой хост-контроллер, к режиму которого приспособиться будет легче всего, в рамках запуска первых тестов нашего eMMC эмулятора.
Всё это в совокупности сильно портит настроение и увеличивает время разработки. Единственное логичное решение в такой ситуации – найти один хост-контроллер, который точно поддерживает выбранный таргет (имеющуюся оригинальную карту eMMC), и повторить настройки этого таргета в своем эмуляторе. После того как получится повторить его функционал с теми же настройками, можно будет пробовать реализовать другие опции, предусмотренные спецификацией.
Как таргет для эмуляции я взял чип от Samsung – KLM4G1FETE-B041. В качестве хоста – SD кардридер, поддерживающий подключение eMMC микросхем через специальный адаптер.
История работы на 3.3V кардридере - поднятие инициализации
Кардридер SD/MMC карт с поддержкой eMMC казался идеальным кандидатом на роль хоста в стенде отладки eMMC-эмулятора. Логично предположить, что кардридеры работают по максимально упрощенной модели (в отличии от интегрированных в процессор IP-блоков хост-контроллеров), так как их спектр задач существенно меньше. Там, где интегрированный блок хост-контроллера будет применять весь спектр MMC-команд и режимов, чтобы отвечать требованиям соответствующих linux-драйверов, кардридеру достаточно лишь обеспечить передачу данных в обе стороны для подключения блочного устройства к SCSI.
Как казалось из дампов с внешнего логического анализатора с подключенной оригинальной картой eMMC, для этого много не надо – достаточно пройти унифицированную процедуру идентификации (которую я в общих чертах описал выше) и начать кидать команды multiblock read/write в обе стороны.
Однако до проверки передачи данных на кардридере дело не дошло. Но тем не менее удалось пройти вышеупомянутую SD/MMC инициализацию без чтения CSD что позволило увидеть эмулятор в списке блочных устройств хост-системы:

Но первая настоящая победа над хост-контроллером была достигнута, когда удалось заставить Vivado адекватно синтезировать проект c IOBUF и избавиться от поведенческих ошибок на командном модуле. После этого я наконец смог передать CSD регистр, что позволило покрутить размер памяти туда-обратно:


Таким образом мы оставили позади уже два пункта: мы не только убедили хост в том, что эмулятор не является SD картой, а принадлежит семейству *MMC, но еще и заставили его принять наш регистр CSD, описывающий размер MMC устройства, то есть намеренно «не дошли» до eMMC стандарта с его Extended CSD (и пока что не реализованным 2+ ГБ размером накопителей).
Но смонтировать эмулятор в кардридере для передачи данных оказалось невозможно – шина данных упорно отказывалась работать нормально. Чем дальше я заходил в тестах, зачастую использующих множество разных кардридеров, тем больше формировалось ощущение, что я тестирую именно сами хост-устройства, а не свой эмулятор.
Проблемы было, по сути, две:
Не хватало обратной связи от самого кардридера. Ближайшее что у меня было к логам – это сообщения о подключениях/ошибках в dmesg. Этого было достаточно, лишь чтобы отметить прохождение вышеупомянутых двух проверок, но не более;
Не хватало возможности поменять механизм работы кардридера. Например, запретить оригинальной карте работать на высокой скорости, чтобы при помощи логического анализатора понять, какое поведение оригинальная карта будет демонстрировать в тех условиях, которые я смогу повторить.
Из-за этого скорость отладки замедлилась почти до нуля. Нужно было отличать следующие 4 вида ошибок:
Ошибка нехватки реализованного функционала (так, например, я ввел калибровку шины данных по BUSTEST CMD19+CMD14, чтобы приблизиться по набору команд на вейвформе к дампу с ЛА с оригинальной картой)
Ошибки при реализации уже предусмотренного функционала (по сути, те, с которыми я сражался в первой части, вызванные непосредственно недочетами в разработке: Clock Domain Crossing, Multiple Driver, и т.д.)
Физические проблемы на шине, вызванные сделанным «на скорую руку» стендом
Ошибки уже самой среды разработки: не вычищенные кэши, съехавшие тайминги, нехватки памяти, и т.д.
И вот за этими 4 ошибками я часто не замечал 5 вариант – хост мог просто фактически не поддерживать этот функционал в заданных мной ограничениях скорости работы. Но понимание этого придёт существенно позже.
Отладка эмулятора eMMC на программаторе – поднятие корректного чтения регистров
Программатор eMMC, по идее, мог исправить одну из двух главных проблем кардридера как тестового хоста – мог выдать достаточно подробный лог взаимодействий с эмулятором, чтобы я мог быстрее классифицировать ошибку. Как хост для проверки эмулятора был взят программатор XGecu:

Быстро стало ясно, что использовать «упрощенный» MMC протокол не получится – XGecu упорно рвался прочитать мой Extended CSD. Протестировав функцию в тестбенче, мне удалось получить подтверждение чтения ECSD на программаторе:

Но данные он прочитать не смог, хотя в симуляции это сделать получалось.
В общем, я начал копать какой набор настроек оригинальной карты был необходимым, а какие настройки можно было «скрутить» для уменьшения скорости соединения. Процедура инициализации eMMC полагается на большое количество значений CSD/ECSD и, учитывая, что я отработал корректное чтение CSD, логично было бы:
Отработать инициализацию с чтением и записью ECSD по такой же схеме;
Добавить в дизайн систему runtime-реконфигурации значений всех регистров:

AXI GPIO IP (не имеет отношения к внешним GPIO банкам) – простой блок, дающий перезаписываемый через AXI Lite регистр, идеально подошел для констант. Так как шириной в 128 бит он не поставляется (возможно к лучшему, учитывая какие трудности в имплементации бы это создало), я разбил CSD/CID по 4 блока на каждый.
True Dual Port BRAM – блочная память, способная работать на двух источниках тактирования (как раз для нашего случая, где конфигурация задается при помощи источника тактирования софт-процессора, а чтение происходит по мастер-клоку eMMC).
Microblaze Soft-Processor IP – про него тоже можно сказать немного. Я даже не прошивал его, пользуясь только командами чтения и записи слова в отладочной консоли (mwr/mrd) для изменения отдельных конфигурационных значений в памяти/регистрах через общий интерконнект. Отладочная консоль (XSCT) работала через JTAG.
Разумеется, как и всегда с ПЛИС, были «свои славные битвы», особенно с не задокументированным поведением True Dual Port BRAM с разной шириной выходов (технически он её поддерживает, но при включении разной ширины слетает адресация на 32-битном выходе, что ломает BRAM Controller), потом еще были сбои при чтении этим контроллером памяти (транзакции чтения и записи через AXI шли на слово вперед, когда выход на мастер-клоке работал абсолютно корректно). Первые столкновения с ошибками clock domain crossing (CDC) – если BRAM разделяет клок-домены самостоятельно, GPIO надо синхронизировать вручную. Но, конечно, эти проблемы не сравнятся с тем, что меня ждало при создании настоящего буфера памяти для эмулятора. Но это уже хотя бы было после того, как путь к отладке шины данных был наконец открыт.
А пока проблемы было две:
Программатор дает очень мало логов, и это не исправить.
Подключать эмулятор к программатору неудобно и ненадежно. Длинные провода, странный разъем для подключения. Отсюда сразу куча проблем.
Из доступных хостов, способных решить эти проблемы, оставался только один вариант.
Отладка эмулятора на Orange Pi 3B – чтение и запись данных
eMMC – микросхема, которая должна быть припаяна на плату. Это, в принципе, следует из названия: embedded MMC. Высокие скорости, параллельные линии – все это мешает завести эмулятор на проводах в воздухе, подключенных к посадочному месту микросхемы eMMC. Именно поэтому я так старался подружить его с кардридером – любая возможность сократить путь сигнала от GPIO банка до хост-контроллера была бесценна.
Но есть исключение – некоторые производители одноплатных компьютеров используют специальные модули, где eMMC микросхемы устанавливаются на миниатюрную плату, позволяющую быстро менять модуль памяти одноплатника. Такое решение впервые дает нам разъем к "настоящей" шине eMMC, не ограниченной по ширине или рабочей частоте, в отличии от рассмотренных выше универсальных SD/MMC слотов.

В качестве хоста был выбран распространенный и недорогой одноплатник Orange Pi 3B, поддерживающий такие модули. Помимо этих преимуществ, он также имелся в наличии, а заодно предоставлял хорошую первую задачу для проверки функционала эмулятора (об этом будет ниже).
Начать с ним работать мешали только две проблемы:
Требуется переключить эмулятор на 1.8V напряжение (синтезировать дизайн под другой банк GPIO, который запитан именно таким уровнем питания);
Нужно разработать плату-переходник, которая была бы совместима с разъемом eMMC Orange Pi, но вместо микросхемы шлейф бы шел с нее в эмулятор;
Первое, разумеется, было не сложно – на отладочной плате ПЛИС как раз был подходящий банк пинов.
Второе, конечно, немного сложнее, тут помогли коллеги. Мы изготовили две платы, которые соединяются между собой шлейфом. Это давало необходимую для соединения гибкость. Одна плата подключалась в Orange Pi через разъем для подключения eMMC, и служила переходником на шлейф. Вторая плата служила переходником от эмулятора на шлейф.
Внешний вид стенда:

Для этого стенда главным источником логов был UART одноплатника. В него летят все сообщения maskrom, SPL, U-Boot и ядра. Но сначала стоило попробовать посмотреть, что по командам на ILA – изменений было немало.
Первое отличие: своя процедура настройки ширины шины
Первое с чем я столкнулся, пытаясь пройти инициализацию на новом хосте – как сильно этот процесс отличается от заявленного в спецификации с первых же команд. Хост здесь нацелен на использование eMMC карт и, хоть он для приличия и проверяет эмулятор SD-шным диалогом, можно было увидеть, что многие части спецификации, нацеленные на обратную совместимость, он пропускает мимо.
Так, например, он не заморачивается калибровкой линий. Вместо этого у него своя процедура проверки возможности работы на нужной ширине:
1) После базовой инициализации на 1-битной шине он приказывает eMMC переключиться на самую большую ширину шины (8 бит);
2) Далее он проверяет успех переключения, вычитав регистр ECSD через шину данных с заданной шириной;
3) После этого он принимает решение остаться на этой ширине или, если вычитанный регистр не выдерживает сравнения с прочитанным ранее на 1-битной шине, откатиться на другую ширину – 4 бита;
4) Если и на 4 битах его не устраивает чтение ECSD, то он отказывается иметь дело с картой вовсе;
Оригинальная карта, очевидно, принимает такое поведение за норму. Потребовалось добавить поддержку 4 и 8 бит в блок:

На схеме можно видеть, что помимо новых IOBUF, для каждой линии был инстанцирован отдельный генератор CRC – это позволяет генерировать и проверять контрольную сумму согласно спецификации, по набору CRC16 на каждую задействованную линию.
Второе отличие: Отказ работать без High-Speed режима
Следующее препятствие, с которым я столкнулся – хост определенно игнорирует настройки максимальной частоты в CSD, сразу пытаясь выйти на какой-то из разрешенных в ExtCSD High Speed режимов. Если запретить ему все High Speed режимы в ECSD, то он в принципе откажется запускаться (не только не откатится до настроек из CSD, но даже не продолжит работу на 400 кГц). Это решается просто – редактируем ECSD, разрешая минимальный (26 МГц) High-Speed режим.
Третье отличие: Open-ended Multiple Block Read/Write
Третье отличие намного более существенное, и потребовало серьезной отладки. Для начала, была поймана вот такая ошибка:
sdhci_transfer_data: Error detected in status(0x108000)!
В ответ на это был модифицирован SPL в mmc.c – помимо уже имеющегося там режима «#define DEBUG» было добавлено логирование всех данных, читаемых и записываемых в карту.

Пропатчив SPL, я запустил на нем оригинальную карту. Тогда прилетел следующий лог в UART:
... CMD_SEND:16 <= [команда задания длины блока (не количества блоков)] ARG 0x00000000 MMC_RSP_R1,5,6,7 0x00000000 CMD_SEND:18 <= [команда чтения конечного количества блоков] ARG 0x00000000 MMC_RSP_R1,5,6,7 0x00000000 DATA received: <block_0> <block_1> ... <block_n> CMD_SEND:12 [команда остановки чтения] ARG 0x00000000 MMC_RSP_R1b 0x00000000 ...
Худшие опасения подтвердились. Open-ended режим записей и чтения, позволяющий неограниченные запись/чтение по указанному в аргументе адресу до остановки командой, изначально не был предусмотрен, так как при работе из Linux не наблюдалось случаев его использования. Однако в SPL/U-Boot ситуация иная. Здесь удалось добиться исчезновения ошибки чтения памяти только после введения этого режима, после чего ошибки уже отсылали к некорректности содержимого памяти, а не процесса чтения:
... CMD_SEND:16 ARG 0x00000200 MMC_RSP_R1,5,6,7 0x00000900 CMD_SEND:17 ARG 0x00000000 MMC_RSP_R1,5,6,7 0x00000900 DATA received: <block_0> CMD_SEND:16 ARG 0x00000200 MMC_RSP_R1,5,6,7 0x00000900 CMD_SEND:17 ARG 0x00000000 MMC_RSP_R1,5,6,7 0x00000900 DATA received: <block_0> No partition table - mmc 1
Данные были прочитаны (я убрал их для компактности, но там те самые блоки, которые я загружал в BRAM раннее). Значит пора подсовывать ему настоящую прошивку и двигаться в U-Boot. Но для этого нужно было больше памяти, чем мог дать BRAM Memory Generator.
Реализация буфера памяти эмулятора eMMC при помощи контроллера DDR4 ОЗУ и AXI Master
Далее требовалось решить проблему расширения памяти эмулятора. Сначала рассматривались варианты с энергонезависимой памятью – это бы дало возможность тратить минимальное количество времени на перезапись образа:
eMMC/SD хост. Но я прикинул скорость и оказалось не так круто, как хотелось бы. Плюс - эти блоки тоже придется писать с нуля, хоть и с очевидными наработками от эмулятора карты.
NVMe хост, который использует внешний диск как буфер памяти. Проблема в сложности протокола, закрытых и платных блоках хост-контроллеров и в таймингах eMMC. Могла возникнуть ситуация, что малоразмерные записи/чтения блоков eMMC просто не будут укладываться в тайминги eMMC стандарта и eMMC драйвера в Linux.
Ну раз с энергонезависимой памятью такая ситуация, посмотрим варианты, которые потребуют поднимать какую-то систему инициализации данных:
На всякий случай уточню: BRAM/URAM/и т.д. не подходят на несколько порядков. Максимум что можно выжать из них на самых дорогих кристаллах – несколько мегабайт памяти.
Другой вариант внутри-кристальной памяти – HBM – предполагает слишком маленький потенциал для расширения, всего до 16 ГБ (в устройствах часто встречаются eMMC на 64 ГБ и больше и эмулятора на 16 ГБ будет недостаточно).
Контроллер памяти DDR4 по скорости практически не отличается от HBM (а на некоторых метриках даже выигрывает!), но при этом не требует использования более дорогих серий ПЛИС с внутри-кристальной памятью и теоретически позволяет расширить буфер, согласно спецификации, до 128 ГБ регистровой памяти.
Таким образом, был выбран контроллер памяти DDR4 (у AMD он называется MIG - Memory Interface Generator) для создания большого высокоскоростного буфера памяти в ПЛИС. Но выбор энергозависимой памяти предполагает наличие какой-то системы инициализации этой памяти. Поначалу планировалось использовать PCIe Bridge, но он показал себя слишком громоздким для отладочных сборок (время сборки резко увеличилось до ~2 часов, почти в два раза), поэтому вместо него был вставлен DMA, подключающий USB 3.0 FIFO в память.
Новая архитектура дизайна получилась примерно следующая:

Я решил реализовать доступ в память через прямые AXI Master транзакции из блока эмулятора, без каких-либо схем префетча при помощи DMA. Для этого есть причина – размеры AXI Burst (до 256 параллельных передач с шириной 32-64 бит) отлично оптимизированы как под возможности MIG, так и под размеры блоков памяти в протоколе eMMC (512 байт). Таким образом я могу использовать burst с длиной 128 и шириной 32 бита, чтобы получить те же 512 байт на запись и чтение. Вот как это выглядит (подробнее про протокол AXI можно почитать здесь):


В общем и целом, когда довольно непростой в своей многофункциональности протокол AXI удается редуцировать до двух приведенных выше операций, реализовать его становится не так сложно. Главная трудность здесь лежит не в протоколе, а в хитрой системе backpressure, позволяющей вовремя обмениваться данными с контроллером памяти. Также её нужно правильно развязать в двух совершенно непересекающихся частотных доменах. Vivado иногда «прощает» легкие нарушения CDC, если они между клок-доменами, сформированными от одного клока. У нас же два совершенно независимых клока, поэтому такой поблажки не будет. Примерно так выглядит модуль AXI Master на уровне внутренних сигналов:

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

То, что я обозначил Internal Memory – это внутренний объем NAND-памяти, разведенной на кристалле микросхемы eMMC, которой управляет контроллер на том же кристалле. Отчасти схема гипотетическая, построенная на основе косвенных намеков в спецификации, но она подчеркивает главное – процедуры записи и чтения, данные протоколом, не предполагают, что карта обладает буфером памяти с нулевой задержкой доступа. Для ожидания записи данных контроллер eMMC-микросхемы может использовать Busy Sequence, а для ожидания начала чтения после ввода адреса – задержку между отдачей R1 ответа по командной линии и подтягиванием start bits на шине данных (максимальная длительность обоих задержек – вопрос скорее эмпирический, учитывая разницу в поведении хост-контроллеров). Вот как эти механизмы используются для работы с сигналами AXI:

Важно уточнить, что левая часть блок-схемы здесь работает по общей тактирующей частоте ПЛИС, а правая - по тактовому сигналу emmc_clk от хост-контроллера. Переход между доменами – причина для ввода Async FIFO. Как следствие, он также работает в пакетном режиме – выброс данных из выхода FIFO начинается лишь тогда, когда FIFO заполняется данными из входа.
Интересна проблематика механизма «постоянного чтения», упомянутого в предыдущей части (Третье отличие: Open-ended Multiple Block Read/Write). Поначалу кажется, что ничего нового здесь придумывать не надо. Просто по завершении каждого AXI-чтения/записи начинать еще одну AR транзакцию. Но дьявол кроется в деталях – что делать, если:
AR транзакция была уже отправлена;
До заполнения FIFO данными из MIG, пришла отмена чтения командой CMD12;
Данные продолжают прилетать после отмены транзакции, заполняют FIFO, и будут выброшены в канал eMMC при следующем чтении, чем заменят блок данных, который на самом деле ожидается в этой транзакции.
В версии под BRAM эту ситуацию я исключил максимально простым способом – сбрасывал вообще все, связанное с буфером (от адреса до самой BRAM) в момент остановки чтения. Такой фокус с MIG не пройдет – сбросив его, я остановлю DDR рефреши и тем самым инвалидирую данные внутри памяти. Как-то вдогонку кинуть AXI-сообщение в духе «не, данных больше не надо, отбой» тоже нельзя, протокол AXI такого не позволяет (eMMC бы стоило у него поучиться!).
В общем, какое-то время я использовал несколько сбросов FIFO, настроенных по времени друг после друга так, чтобы «ну уж точно» сбросить содержимое. Кажется, что для такой (уже маловероятной!) ошибки, вызванной в уже маловероятном сценарии, подобный фикс должен быть надежным решением. Но, конечно, несколько десятков тысяч чтений способны сломать все что угодно. В какой-то момент у меня был выработан отдельный тайминг сброса FIFO для continuous read во время работы SPL/U-Boot и отдельный – под continuous read во время работы maskrom.
Решение пришло позже – конструкция в духе AXI Interposer, которая ставится между моим AXI мастером и AXI интерконнектом на AW шину, считает прошедшие данные и ждет сброса по CMD12. Как только тот появляется, она отклоняет все данные, идущие на FIFO, подменой RREADY и не дает послать AR-запросы, подменяя ARREADY, до тех пор, пока счетчик данных не заполнится до размера транзакции.
Так удалось добавить целых 4 ГБ настоящей памяти в эмулятор, что позволило упаковать в неё полноценный образ консольного Linux, позволив загрузиться с Orange Pi.
Orange Pi 3B – продолжение. Сражение с U-Boot и триумфальная загрузка ядра
Благодаря AXI-мастеру, эмулятору удалось пробросить нужные данные в хост и пройти через SPL на частоте eMMC шины 26 МГц и ширине 4 бита (а затем и на 8 бит).
Четвертое отличие: внутренняя ошибка SDHCI при работе на 26 МГц
Далее начался процесс итеративной доработки блока, который остановился на этой ошибке:
CMD_SEND:18 ARG 0x00038398 sdhci_transfer_data: Transfer data timeout RET -70 mmc_bread: Re-init mmc_read_blocks error Re-init retry timeout Error reading cluster Unable to read file /uInitrd
Из подробного изучения дампов UART, логического анализатора и т.п. можно было с уверенностью сказать – оригинал и эмулятор вели себя абсолютно одинаково ровно до этой ошибки, за исключением одного аспекта – скорости работы. То есть данные эмулятор выдавал корректно и практически в те же, если не меньшие, сроки, чем оригинал. Эмулятор все еще работал на high speed режиме в 26 МГц, а оригинал – на 200 МГц. По идее, влиять на работу SDHCI это не должно было, т.к. ошибка, которую мы наблюдаем выше, это именно ошибка IP-блока хост-контроллера. Отбраковка по скорости/ширине шины – это ситуация, типичная для процесса инициализации, а тот был уже далеко позади – мы наблюдаем активную загрузку ядра (на что намекает строка про uInitrd в логе выше).
Но больше вариантов не оставалось, кроме как сравнять скорость. Логичнее сделать это в сторону уменьшения – запретить оригинальной карте переходить на 200 МГц. Сделать это надо было через модификацию того же драйвера в U-Boot, т.к. редактировать DEVICE_TYPE настоящих карт eMMC нельзя (это позволяет вспомнить, зачем вообще был нужен эмулятор..).
Каково же было мое удивление, когда после накатки патча и запуска Orange Pi с оригинальной картой консоль бодро сообщила о переключении на 26 МГц... и успешной загрузки в ядро! Ситуация максимально комичная – два устройства, ведущих себя одинаково на всех проверках и чтениях, демонстрируют разную реакцию от U-Boot на этапе загрузки ядра: эмулятор вызывает таймаут, а оригинальная карта спокойно загружает ядро до конца.
Выяснилось, что проблема конкретно этого опыта была в самом механизме загрузки Orange Pi. В общем, он технически позволяет загружать SPL и U-Boot как из SPI Flash, распаянного на плате, так и из eMMC. При заливке образа в eMMC бинарные файлы SPL и U-Boot тоже попадают в образ. Так какой будет выбран? Если вы подумали про те же два варианта:
Либо они оба будут загружены в приоритете из SPI Flash;
Либо они оба будут загружены из eMMC, при возможности, а затем из SPI Flash, если загрузка из eMMC не прошла (более вероятно, т.к. это имеет смысл с точки зрения производительности);
То оба варианта не соответствуют действительности. Вот как это происходит на самом деле:

Разумеется, патч я положил только в SPI Flash, что чудесным образом позволило мне увидеть успешное понижение частоты в SPL... а после карта просто вернулась обратно на HS200 в U-boot и обошла баг стороной! Как несложно догадаться, когда я обнаружил это и пропатчил еще и U-boot на eMMC карте, произошла совершенно та же ситуация, что была с эмулятором, но уже с полностью рабочей оригинальной картой:
CMD_SEND:18 ARG 0x00038398 sdhci_transfer_data: Transfer data timeout
В общем да, теперь можно быть уверенным: U-Boot Orange Pi не поддерживает 26 МГц на eMMC карте. Причем, ковыряясь в коде, возникает подозрение, что проблема не столько в коде самого U-Boot, сколько в хардварном SDHCI контроллере (вот здесь можно видеть, что таймаут для данных задается размером в 10 секунд – из вейвформ я знаю, что таких задержек там не было и в помине).
Досадно, но ничего не поделаешь, 26 МГц обкатать на этом хосте не выйдет. Но зато, когда я переехал на 52 МГц, U-Boot все-таки смог загрузить ядро Linux. Что еще раз доказывает, что дело было в таймингах SDHCI контроллера.
Заключение
Дальше работа уже пошла существенно бодрее:
При помощи эмулятора удалось проэксплуатировать TOC-TOU уязвимость в RK3566 и таким образом обойти Secure Boot (подробнее об этом читайте в нашей следующей статье);
Получилось выйти на скорость HS200;
Начал планировать добавить поддержку DDR режимов eMMC.

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

VelocidadAbsurda
06.08.2026 08:29Вдруг пригодится: встроенные контроллеры eMMC, которые смотрел (Phison, Appotech) обрабатывают большинство команд софтово: приняли команду, попали в ISR, там её спарсили, выставили статус на отправку, протранслировали LBA в физический адрес NAND (при этом, возможно, читали NAND, чтобы заглянуть в таблицу трансляции), запустили чтение NAND, дождались готовности, настроили DMA между NAND и MMC интерфейсами, дали старт, и вот только тут пошли данные в сторону ММС хоста, и он всё это терпел. Т.е. тайминг команда-данные заведомо не такой уж жёсткий. Особенно учитывая, что всякие «унылые» QLC NAND могут с первой попытки нормально не прочитаться, потребовать более «агрессивной» коррекции (чтение с разными уровнями Vtt и прочие танцы с бубном), и приличные данные из них вылезут и того позже. Ну и сами контроллеры - отнюдь не гигагерцовые монстры, у них и RAM под всю прошивку не у всех хватает, подгружают overlay’и в процессе (опять же из NAND). Масса неопределённостей в общем, но хоста это как-то устраивает.

Rou7e Автор
06.08.2026 08:29Согласен, у меня такое же впечатление сложилось от взгляда на слитые прошивки для FFU - что командный модуль это какое-то слабое процессорное ядро, работающее по прошивке, а данные обрабатываются именно что специализированными DMA между NAND и eMMC интерфейсом, которые настраиваются тем самым ядром. Все на это указывает.
Но там не обошлось без хитростей. Например, если обратить внимание на работу того многострадального open-ended read, спека задает тайминг между стоп битом команды остановки чтения, и собственно остановки чтения на шине данных. Тайминг этот Nst, и равен ровно 2 циклам (6.15.7 JESD84-B51.20). И это оставляет немой вопрос - а как этот процессор так точно выключает свои DMA с точностью до такта, когда он сам явно работает последовательно? Тут варианта три:
1) Либо все забивают на тайминг из спеки и прерывают читалку когда смогут (вероятнее всего)
2) Либо процессор заранее планирует стоп передачи, запуская таймер сразу по получению команды (не дожидаясь её завершения, для проверки CRC например)
3) Либо этот случай вообще не обрабатывается прошивкой и ядром, и обрабатывается как-то через логику в порядке исключения.
В общем нарисовать стейт машину для команд оказалось проще и надежнее, чем реализовывать такое же софт ядро (не было риска откатиться при попадании на нереализуемые требования). Хотя преимущества софта тоже понятны)
VelocidadAbsurda
06.08.2026 08:29Да, выглядело как будто бы часть команд (самые простые вроде чтения OCR/CID/CSD) обрабатывается прямо железом, их софтовые обработчики пустые. Подозреваю, команда STOP TRANSMISSION обрабатывается и аппаратно (сразу тормозит передачу данных на ММС интерфейсе), и дальше софтово (отмена текущей операции везде, где нужно - сбросить DMA, вывести NAND в состояние ничегонеделания, ну и собственно выход из цикла обработки очередных секторов). Достоинство такой архитектуры - гибкость, в софте всё-таки проще делать что-то «нелинейное», а вашему проекту, насколько я его понимаю, в будущем гибкость ещё нужнее, чем настоящим eMMC. К примеру, дойдёт дело до экспериментов над Андроидом, понадобится эмуляция RPMB, а там весь протокол - на уровне выше, пакеты с командами/параметрами/статусами внутри данных секторов.

checkpoint
06.08.2026 08:29Шикарная статья, спасибо!
Имел небольшой опыт реализации SD контроллера "на минималках" для ПЛИС Lattice ECP5 на SpinalHDL. Замаялся с поддержкой различных SD/MMC карт - у всех свой набор поддерживаемых протоколов. Всю Вашу боль хорошо понимаю.

DrMefistO
06.08.2026 08:29Планируется ли выкладывание исходников в паблик?
А то опыт, конечно, классный, только пользы от него кому-то, кроме лабы, ровно 0. Если кто-то возьмётся повторять, то это те же 18 месяцев или больше.
15432
Сколько суммарно ушло на разработку этой штуки? Мой знакомый уже третий год пилит нечто похожее по сложности, SD->IDE переходник, интересно сравнить
Rou7e Автор
Ушло приблизительно 18 месяцев работы, от начала разработки до загрузки в ядро. Терпения и сил знакомому, если это SD с поддержкой UHS, там возможно задача еще сложнее)
15432
Хлеще, он добавил поддержку SD Express. А с другой стороны поддержку проприетарных ATA команд рекордера Sony PSX