Более чем два месяца назад заказал отладочные платы для своего хоббийного проекта, но они так и не пришли и хз, придут ли вообще. Разработка прошивки для него из-за этого застопорилась: писать «на воздух» я не особо люблю, а текущая клавиатура уже не отвечает моим требованиям - банально не хватает кнопок.
И тогда в голову начала приходить мысль - а не организовать ли мне эмулятор по типу того, который используется в Android Studio? Идея показалась мне классной, но останавливало отсутствие внятной документации по qemu, поэтому для реализации задумки предлагалось соснуть сурца. В какой-то момент я понял, что без полноценной клавиатуры я не смогу продолжить проект - банально нет всех нужных кнопок - было принято решение засучить рукава и попробовать.
В штатной поставке esp-idf есть qemu, но в ней особо не развернёшься, поэтому начал с простого - используя простое i2c устройство в качестве шаблона, создал заглушки для клавиатуры, контроллера модема и контроллера питания. Подключить их тоже не составило труда - код как раз на скриншоте. Обмен данными по шине i2c оказался очень простым: на эмулируемом устройстве есть всего 2 функции обратного вызова - для чтения и для записи. Таким образом, когда мастер пишет и читает, то просто подсовываем ему то, что он просит. Таким образом реализовать эмуляцию i2c устройств оказалось легче лёгкого.
Далее прерывания. Для эмуляции линий gpio в qemu используются линии прерываний. Тут вроде всё просто: на эмулируемом устройстве создаются выходные линии qemu_irq и далее они подключаются… куда? Вот тут меня ждала первая, пусть и лёгкая, задача. В самом начале я подключил линию irq клавиатуры на матрицу прерываний. Обработчик прерываний вызывался, но понять, на каком gpio оно произошло он не мог. Немного пораскинув мозгами и пососав сурца покопав исходники, я нашёл правильное место для подключения - матрица gpio. Как видите, в функциях read и write ничего нет, поэтому реализовать матрицу предлагается самостоятельно. Ожидаемо :) Реализовывается она очень просто. Сначала в матрице gpio создаются 40 входных и 40 выходных лини qemu_irq, которые будут отвечать за все gpio, которые есть у esp32. Далее создаются ещё 2 выходные линии qemu_irq, которые отвечают за обычные и non-maskable прерывания. Их я подключил к матрице прерываний на специально предусмотренные для этого каналы.
Теперь возвращаемся к функцуиям read и write. Вы когда-нибудь работали с расширителями портов, которые подключаются по шинам spi и i2c? Вся суть сводится к тому, что вы просто пишете и читаете нужные регистры по шинам, верно? Вот тут всё тоже самое, только с одним маленьким отличием - сами регистры нахадятся в самом микроконтроллере и процессор имеет к ним прямой доступ. То есть матрица gpio - это просто расширитель портов, который встроен в процессор и подключен напрямую к нему. Таким образом, вы теперь выступаете в качестве микросхемы :) В ваши регистры будут что-то писать и читать, а вы будете управлять линиями gpio. Когда процессор хочет выставить нужный gpio в нужный уровень - он просто пишет нужный бит в нужный регистр. Вы это видите и переключаете нужную линию qemu_irq в нужный уровень. Если же входная линия изменяет свой уровень, то вы просто отмечаете это у себя в структуре и при чтении сможете вернуть процессору текущие уровни gpio. Если процессор настроил прерывание на каком-либо gpio, то его можно уведомить об этом с помощью линий irq и nmi-irq. Далее процессор прочитает регистр прерываний и попросит вас сбросить их и вы это сделаете :) (иначе он будет постоянно опрашивать регистры прерываний, думая, что они всё ещё активны). Таким образом, реализовать матрицу gpio не составило особого труда. После этого я подключил свои устройства к нужным линиям матрицы gpio и смог эмулировать работу gpio без проблем.
Итак, я уже потирал руки и думал: «Сейчас я кабанчиком организую эмуляцию дисплея на шине spi и можно будет браться за гуй.». Ок, по аналогии с устройством i2c, беру в качестве шаблона какое-нибудь spi-устройство и начинаю реализовывать дисплей. Общение происходит как по i2c, только вместо обратных вызовов read и write, у вас есть обратный вызов transfer, в котором одновременно происходит и приём и передача данных. Сделал я дисплей, запускаю и… тишина! То есть по шине мне передаются сплошные нули! Всмысле? 0_0 В этот раз пососать сурца покопаться в исходниках пришлось гораздо более основательно. В итоге, понатыкав везде и всюду спасительный printf(), я нашёл чёрную дыру, в которой терялись данные по дороге от гостя до эмулируемого дисплея. На первый взгляд кажется, что тут всё ок, но когда я расставил printf() для распечатки адресов регистров, которые пишет и читает процессор, то всё встало на свои места - этот код никаким образом не обрабатывает регистры DMA! То есть вообще. Получается, что гость пытается слать данные через DMA буфер, а ванильный эмулятор spi туда даже не смотрит. В итоге, я сделал его копию и написал свою реализацию, которая позволяет отправлять данные по spi с использованием dma (полную реализацию пилить не стал, ибо влом). И для этого пришлось решить задачу со звёздочкой - доступ к памяти гостя.
Пару слов о том, как работает DMA на esp32. Прошивка организовывает где-нибудь в памяти специальные структуры с указателями на сами данные и входной буфер. Далее в специальнные DMA-регистры записываются адреса (указатели) этих структур. Далее подаётся команда на запуск передачи и данные передаются/принимаются без участия процессора. Теперь передо мной встала задача - как получить доступ по указателям, которые мне передаёт гость? Ведь гость даёт адрес внутри адресного пространства виртуальной машины. Сначала я нашёл решение с помощью функций cpu_physical_memory_read() и cpu_physical_memory_write(), которые работают с указателями из адресного пространства гостя. Но лишнее копирование не давало мне покоя и я нашёл решение получше.
При создании виртуальной машины qemu создаются различные области памяти. Одна из них - оперативка. Создаётся с помощью memory_region_init_ram(). В esp32 создаётся несколько различных областей такого типа. Фишка в том, что все эти области выделены как обычная память на хосте. Идея заключается в том, чтоб использовать memory_region_get_ram_ptr(), чтоб получить указатель на каждую область. Затем, зная начальный и конечный адрес каждого региона, можно вычислить расположение данных в оперативке хоста и работать с ними напрямую.
Последняя задача - как передать эти указатели, которые возвращают вызовы memory_region_get_ram_ptr(), в сам эмулятор spi? Он выполнен как отдельное устройство. В этот раз сосание сурца ковыряние в исходниках и чтение крох документации ничего не дали, поэтому решил действовать в лоб - передавать через свойства устройства. В эмуляторе spi я сделал несколько свойств с помощью object_property_add(), при создании машины при инициализации spi просто передал указатели как uint64_t с помощью qdev_prop_set_uint64(). Не уверен, что поступал правильно, но других решений не нашёл. Возможно, в таких случаях следует создавать отдельное устройство - эмулятор DMA, не знаю. Тем не менее, это сработало - я смог получить доступ к памяти гостя, высчитав смещение и прибавив его к одному из указателей на область памяти.
Гуй сделал на Tcl/Tk. Общение в машиной идёт через fifo. Просто и понятно. Уверен, что у qemu есть свой api для построения гуя, но я, честно говоря, к этому моменту уже окончательно устал искать информацию раз, и хотелось немного попрактиковаться в тикле два. До этого в тикле не работал с цветными изображениями и это был неплохой повод попробовать. Оказалось, что всё делается элементарно через image. Благо по тиклю информация есть, в отличие от.
К чему я вообще пишу обо всё об этом? Похвастаться? Да, не без этого, но ещё больше хочется поделиться вот чем. До того, как я полез писать эмулятор, я многое знал, но не понимал как именно работают те или иные вещи в микроконтроллерах. После того, как я написал эмулятор, я многое понял. Например, как именно процессор управляет своей перефирией. Как работают прерывания. Как работает DMA. Да и вообще, получил опыт в работе с «сырыми» регистрами. Я это к тому, что если у вас есть какая-нибудь идея и вам хочется её осуществить, то всегда пробуйте! Даже если задача изначально кажется сложной и неподъёмной. Запросто может оказаться так, что вы, как и я, не просто реализуете задуманное, но ещё получите ценные знания и/или опыт в процессе.
Такие дела \ё/