LANDAU: Мы сделали BE-U1000 в QEMU

LANDAU (Linux And Neo-Depositorium Associated Unity) — это зонтичный проект RULKC, который объединяет усилия российского open-source сообщества вокруг ключевых системных компонентов: ядро (LANDAU Linux), загрузчик (LANDAU U-Boot), UEFI (LANDAU EFI), эмуляция и виртуализация (LANDAU QEMU) и другие. Название — в честь Льва Ландау: как он собирал физиков вокруг новых идей, так LANDAU собирает разработчиков вокруг общей технологической базы.

Подробности и ссылки — на landau.one.

Модель BE-U1000 — одно из первых крупных направлений LANDAU QEMU. Что это значит на практике:

  • Если хотите просто пользоваться — уже сейчас можно собирать примеры из mcu-sdk-examples и гонять их в QEMU: UART, CLIC, MTIMER, CRU/RESET и semihosting работают.

  • Если хотите поучаствовать — в таблице ниже есть задачи любой сложности: от GPIO и WDT до DMA, CAN FD и MicroPython. Простая задача — та, у которой есть пример в SDK и критерий готовности понятен заранее, следовательно спорить о нём не придётся. За собой задачу можно застолбить письмом на rulkc@linuxtesting.org — мы с Ильей Чичковым (@redeyedev) поможем, подскажем по коду QEMU и закрепим модуль за вами. Особенно хорошо, если по итогу появится небольшая статья, задокументированный опыт по написанию модуля для QEMU ценен.

  • Если никто не возьмётся — ничего не встанет, будем писать по модулю не реже раза в месяц и каждый выкладывать с пояснением. Проект не зависит от того, найдётся ли соавтор, но с соавторами он пойдёт заметно быстрее и интереснее.

BE-U1000

Короткая справка для тех, кто впервые слышит об этом чипе.

BE-U1000 (Baikal-U) это российский 32-битный трёхъядерный микроконтроллер на открытой архитектуре RISC-V. Внутри два ядра от CloudBEAR BR-350 (до 200 МГц) и одно BM-310 (100 МГц), 256 КБ eFLASH, 192–202 КБ SRAM с TCM-памятью, а также широкая линейка периферии: 8 UART, 4 I²C, 2 CAN FD, USB 2.0 OTG, три 12-битных АЦП и блоки для управления электродвигателями, а также Микро-Питон (что бы это ни значило).

Структурная схема BE-U1000Структурная схема BE-U1000

Именно для него модель в QEMU мы и делаем — чтобы разрабатывать и запускать прошивки без живой платы (получить ответ на вопрос “Зачем?”, можно в статье GD32 Unleashed: тотальная эмуляция в QEMU).

На мой взгляд он интересен еще тем, что:

  • в наличии есть два CLIC (Core-Local Interrupt Controller), причём per-hart не только CSR, что и так понятно, но и MMIO-регионы для управления;

  • гетерогенность — три харта двух разных типов: два BR350 (CORE0/CORE1) и BM310 (CORE2), причём некоторые особенности CORE0/CORE1 наводят на мысль, что и они специально сделаны для того, чтобы запускать на них независимые прошивки;

  • mailbox, редко встретишь на микро-контроллерах;

  • минимальные отличия от спеки RISC-V, всего несколько кастомных CSR (nmitvec 0x7c0, xccfg 0xbc8, xicfg 0xbd8);

Карточка проекта

Что

модель SoC BE-U1000 и машина EVU-BA-2.5 (-machine evu-ba) для QEMU

QEMU

landau/qemu, ветка n.shubin/landau/be-u1000-rc0

Примеры

mcu-sdk-examples, 58 примеров

Обёртка

qemu-be-u1000-suite репозиторий с QEMU и примерами как submodules (qemu/, examples/mcu-sdk-examples/) и скриптами gdb-be-u1000.sh, be-u1000-dfilter.py; svd2qemu.py;

Сборка

QEMU: riscv32-softmmu → qemu-system-riscv32; примеры: gcc-riscv64-unknown-elf + picolibc-riscv64-unknown-elf

Статус

готовы BR350 CORE0/CORE1, CLIC, CRU/RESET, MTIMER, UART; остальная периферия — заглушки

Контакт

rulkc@linuxtesting.org

Что уже реализовано

Блок

Что сделано

BR350 × 2 (CORE0/CORE1)

У каждого харт своё приватное адресное пространство (cpu0_memory/cpu1_memory) в котором: свой ROM (ROM0/ROM1), свой CLIC и MTIMER — на одном и том же локальном адресе, но физически разные устройства.

BM310 (CORE2)

Формально существует: CPU-тип TYPE_RISCV_CPU_CLOUDBEAR_BM310, объект bm_cpus создаётся в SoC. Но на данный момент — нет своего CLINT, нет MTIMER, нет приватного адресного пространства, то есть реально харт не изолирован и прерывания до него не доходят.

CLIC

Своя копия на CORE0 и CORE1, draft 0.10 (без Debug Triggers и Indirect CSR — как и было задумано для be-u1000).

CRU/RESET

Полное дерево тактирования (GATE/DIV/MUX/PLL) по блок-схеме SoC; Reset Controller как часть CRU (регистры SYSCR0/1/2): через них CORE1 и CORE2 выходят из холодного старта с резет-вектором, заданным прошивкой.

MTIMER (CLINT-совместимый)

Per-hart view на CORE0/CORE1 того же типа, что и CLIC — один backing store, разные приватные адресные пространства

UART (базовый)

Все 8 UART с адресами 1:1 со SVD, прерывание каждого разведено на оба CLIC, какой физический UART становится консолью — настраивается (-machine evu-ba,uart=uartN), а не зависит от порядка -serial.

Архитектура модели (что подключено к чему)Модель BE-U1000 в QEMUМодель BE-U1000 в QEMU

У CORE0 и CORE1 свои ROM, CLIC и MTIMER по одному и тому же локальному адресу, но в разных адресных пространствах; периферия на общей шине видна всем. Прерывание каждого UART разведено на оба CLIC, а через SYSCR0/1/2 в CRU CORE1 и CORE2 стартуют с резет-вектором от прошивки.

Первые шаги

Как собрать примеры

Понадобятся репозитории mcu-sdk-examples и QEMU (ветки указаны в карточке проекта).

Вендорский тулчейн от Baikal нам не нужен (на самом деле он от CloudBEAR): достаточно пакетов gcc-riscv64-unknown-elf + picolibc-riscv64-unknown-elf (умеют собирать rv32* через multilib). Если системного пакета picolibc нет под нужный тулчейн, в репозитории mcu-sdk-examples есть PICOLIBC=local, который соберёт picolibc из подмодуля под произвольный TC_PREFIX.

Сама сборка:

mcu-sdk-examples $ make -j$(nproc) # с системным picolibmcu-sdk-examples $ PICOLIBC=local make -j$(nproc) # с локальным picolibmcu-sdk-examples $ ls -la build/*.elf-rwxr-xr-x 1 maquefel users 659760 Oct 10 09:46 build/ADC_DiscontMode_Single_TriggerSW.elf-rwxr-xr-x 1 maquefel users 662556 Oct 10 09:46 build/ADC_DiscontMode_Single_TriggerSW_IT.elf-rwxr-xr-x 1 maquefel users 657568 Oct 10 09:46 build/ADC_RegularMode_Single_TriggerSW.elf-rwxr-xr-x 1 maquefel users 660440 Oct 10 09:46 build/ADC_RegularMode_Single_TriggerSW_IT.elf[...]

make -j$(nproc) в корне mcu-sdk-examples собирает все 58 примеров одним проходом.

Как собрать QEMU

Обычная конфигурация под riscv32-softmmu с добавлением фильтра по cpu --enable-trace-cpu-filter (о нём чуть позже):

$ mkdir build-qemu$ cd build-qemu$ ../qemu/configure --target-list=riscv32-softmmu --enable-trace-cpu-filter$ ninja qemu-system-riscv32 # или просто make -j $(nproc) кому как больше нравиться

Запуск примера с UART

-kernel машиной пока не обрабатывается (это тоже открытая задача — см. таблицу ниже), поэтому прошивку грузим явно в EFLASH через generic loader:

$ build-qemu/qemu-system-riscv32 -M evu-ba -bios none  -device loader,file=examples/mcu-sdk-examples/build/UART_printf.bin,addr=0xa0000000,cpu-num=0,force-raw=on  -display none -serial mon:stdioUART printf exampleUART printf exampleUART printf exampleUART printf exampleUART printf example[...]

Консольный UART выбирается -machine evu-ba,uart=uartN (по умолчанию uart0).

Вместо bin можно использовать elf, так как loader понимает elf формат:

$ build-qemu/qemu-system-riscv32 -M evu-ba -bios none  -device loader,file=examples/mcu-sdk-examples/build/UART_printf.elf,cpu-num=0  -display none -serial mon:stdioUART printf exampleUART printf exampleUART printf exampleUART printf example[...]

Примечание: поскольку be-u1000 гетерогенный лучше сразу привыкать какой именно cpu (вернее чье видение адресного пространства используется) грузит прошивку.

Запуск примера с UART через gdb

build-qemu/qemu-system-riscv32 -M evu-ba -bios none  -display none -serial mon:stdio  -s -S

Где:

-s — сокращение для -gdb tcp::1234

-S — поставить машину на паузу, с которой её может снять gdb, monitor или какие внутренние процессы, например изменение состояния линии gpio.

И тогда:

riscv64-unknown-linux-gnu-gdb -ex 'target remote localhost:1234' examples/mcu-sdk-examples/build/UART_printf.elf(gdb) info threads Id Target Id Frame* 1 Thread 1.1 (CPU#0 [running]) 0xa0000000 in ?? () 2 Thread 1.2 (CPU#1 [halted ]) 0xa0000000 in ?? () 3 Thread 1.3 (CPU#2 [halted ]) 0x00001000 in ?? ()(gdb) loadLoading section .text, size 0x4ab6 lma 0xa0000000Loading section .rodata, size 0x8df lma 0xa0004ab8Loading section .data, size 0x14 lma 0xa0005398Start address 0xa0000030, load size 21417Transfer rate: 171336 bits in <1 sec, 7139 bytes/write.(gdb) cContinuing.[...]

Как видите здесь выбор cpu тоже имеет значение.

Трассировка MMIO: фильтр по CPU и по адресам

SoC гетерогенный (BR350×2 + BM310), поэтому обычной трассировки QEMU “какой регион читали/писали” недостаточно — нужно ещё знать, какой харт сделал обращение (или что это DMA, когда current_cpu == NULL). В штатных событиях QEMU memory_region_ops_read/memory_region_ops_write номер CPU уже есть как параметр (cpu_index), поэтому ничего не пришлось добавлять в код самих устройств, только включить сами события:

$ build-qemu/qemu-system-riscv32 -M evu-ba ...  -trace enable=memory_region_ops_read -trace enable=memory_region_ops_write  -D trace.log

Строчки в trace.log выглядят так:

memory_region_ops_write cpu 0 mr 0x56390a6cfcd0 addr 0x11001000 value 0xd size 4 name 'be-u1000-uart'memory_region_ops_read cpu 0 mr 0x56390a6cfcd0 addr 0x11001014 value 0x61 size 4 name 'be-u1000-uart'memory_region_ops_write cpu 0 mr 0x56390a6cfcd0 addr 0x11001000 value 0xa size 4 name 'be-u1000-uart'

Поле cpu — это номер CPU; значение -1 (в отдельной, не показанной выше строке) — признак DMA/периферийного доступа без текущего CPU (current_cpu == NULL).

Примечание: это внутренний порядковый номер CPU в QEMU (порядок создания в be_u1000_soc_realize()), а не значение CSR mhartid. Для be-u1000 они совпадают, но это стоит понимать, а не считать гарантией по умолчанию.

Живой фильтр по CPU: реализован отдельно, через trace-cpu-filter (system/trace-cpu-filter.c): нужна сборка с -Dtrace_cpu_filter=true (то же, что --enable-trace-cpu-filter в configure выше), дальше, тогда мы можем:

-machine evu-ba,trace-cpu-mask=0x1,trace-dma=off

trace-cpu-mask это именно битовая маска, а не номер харта:

  • бит N разрешает cpu_index=N, поэтому “только CORE0” — это 0x1 (бит 0), а “только CORE1” — 0x2 (бит 1), не trace-cpu-mask=0/=1. Через + можно перечислить индексы вместо ручного ИЛИ битов ("0+1" эквивалентно 0x3).

  • trace-dma включает/выключает показ DMA-доступов (cpu=-1) отдельно от списка хартов, это то самое разделение “CPU vs DMA”, о котором шла речь в начале раздела.

Фильтр по адресам: через штатный -dfilter, но вручную подбирать диапазоны неудобно, поэтому есть вспомогательная пара инструментов:

  1. Дамп карты памяти прямо из SoC (берётся из той же таблицы be_u1000_config[], что и вся периферия в этой статье):

    qemu-system-riscv32 -M evu-ba,dump-memmap=memmap.json,dump-memmap-format=json  -S -display none -serial none -monitor none

    (после дампа процесс завершается). Формат text (по умолчанию) для человека, а json для скрипта ниже.

  2. be-u1000-dfilter.py строит -dfilter по человеко-читаемым именам регионов вместо адресов:

    $ be-u1000-dfilter.py memmap.json UART0,ROM0-dfilter 0x11001000+0x1000,0x40000000+0x10000

    С --merge смежные регионы без разрыва между ними схлопываются в один:

    $ be-u1000-dfilter.py memmap.json --merge UART0,UART1-dfilter 0x11001000+0x2000

    А с -- скрипт не печатает -dfilter, а дописывает его в конец команды и сразу запускает её (через exec):

    be-u1000-dfilter.py memmap.json --merge UART0,UART1 --  qemu-system-riscv32 -M evu-ba ...

    Например, в связке с MMIO-трассировкой на UART_printf в лог попадают только обращения к UART0/UART1, а доступы к CRU (0x14000000) отрезаются:

    $ be-u1000-dfilter.py memmap.json --merge UART0,UART1 --  build-qemu/qemu-system-riscv32 -M evu-ba -bios none  -device loader,file=examples/mcu-sdk-examples/build/UART_printf.elf,cpu-num=0  -display none -serial null  -trace enable=memory_region_ops_read -trace enable=memory_region_ops_write  -D trace.log$ head -4 trace.logmemory_region_ops_read cpu 0 mr 0x55da3d7c0a70 addr 0x11001088 value 0x0 size 4 name 'be-u1000-uart'memory_region_ops_write cpu 0 mr 0x55da3d7c0a70 addr 0x11001088 value 0x7 size 4 name 'be-u1000-uart'memory_region_ops_read cpu 0 mr 0x55da3d7c0a70 addr 0x1100107c value 0x0 size 4 name 'be-u1000-uart'memory_region_ops_read cpu 0 mr 0x55da3d7c0a70 addr 0x11001018 value 0x0 size 4 name 'be-u1000-uart'

Semihosting

Semihosting — механизм, через который программа на целевой платформе пользуется ресурсами хоста (консолью, файлами, часами) через отладчик или эмулятор, не имея собственных драйверов. RISC-V для этого использует спецификацию ARM semihosting.

Вызов оформляется «магической» последовательностью:

slli x0, x0, 0x1f;ebreak;srai x0, x0, 7;

номер операции передаётся в a0, указатель на блок аргументов в a1, результат возвращается в a0.

Доступные вызовы:

Консоль

  • SYS_WRITEC (0x03) — вывести один символ;

  • SYS_WRITE0 (0x04) — вывести строку, завершённую нулём;

  • SYS_READC (0x07) — прочитать символ с консоли хоста.

Файлы хоста

  • SYS_OPEN (0x01) / SYS_CLOSE (0x02) — открыть/закрыть файл (":tt" — консоль);

  • SYS_WRITE (0x05) / SYS_READ (0x06) — запись/чтение буфера;

  • SYS_SEEK (0x0A), SYS_FLEN (0x0C) — позиционирование, длина файла;

  • SYS_ISTTY (0x09) — является ли дескриптор терминалом;

  • SYS_TMPNAM (0x0D), SYS_REMOVE (0x0E), SYS_RENAME (0x0F) — временные имена, удаление, переименование.

Время

  • SYS_CLOCK (0x10) — время с начала выполнения в сотых долях секунды;

  • SYS_TIME (0x11) — текущее время хоста (секунды с 1970);

  • SYS_ELAPSED (0x30) / SYS_TICKFREQ (0x31) — счётчик тиков и его частота.

Окружение и служебное

  • SYS_GET_CMDLINE (0x15) — аргументы командной строки (в QEMU из -append);

  • SYS_HEAPINFO (0x16) — границы кучи и стека;

  • SYS_ERRNO (0x13) — errno последнего вызова;

  • SYS_ISERROR (0x08) — проверка кода возврата на ошибку;

  • SYS_SYSTEM (0x12) — выполнить команду на хосте (в QEMU отключено по умолчанию);

  • SYS_EXIT (0x18) / SYS_EXIT_EXTENDED (0x20) — завершить эмуляцию с кодом возврата, удобно для тестов в CI.

Всё вышеперечисленное работает и в QEMU, и, например, через OpenOCD.

На практике всё это часто скрыто за libc: при сборке с --specs=semihost.specs (newlib/picolibc) printf, fopen, time и exit сами превращаются в эти вызовы.

Включение в QEMU:

$ build-qemu/qemu-system-riscv32 -M evu-ba -nographic -semihosting-config enable=on,target=native -device loader,file=examples/mcu-sdk-examples/build/Semihosting.elfSemihosting printf exampleSemihosting printf exampleSemihosting printf exampleSemihosting printf example[...]

С target=native вызовы обрабатывает сам QEMU, с target=gdb они уходят в подключённый GDB, а target=auto (значение по умолчанию) решает один раз, при первом semihosting-вызове: если к встроенному gdbstub в этот момент подключён GDB, вызовы идут в GDB, иначе их обрабатывает QEMU. Выбор запоминается: GDB, подключённый позже, вызовов уже не получит, а после отключения GDB вызовы снова обрабатывает QEMU (gdbstub/syscalls.c, use_gdb_syscalls()). Опция chardev= перенаправляет консольный вывод в нужный chardev.

Генератор из SVD

Идея — Алексея Стариковского : раз у вендора есть CMSIS-SVD с описанием всей периферии, не надо переписывать адреса, регистры и битовые поля руками — их можно сгенерировать. Именно так сделана таблица be_u1000_config[], на которую ссылается вся статья, и именно поэтому адреса в QEMU совпадают со SVD.

svd2qemu.py читает SVD через cmsis_svd (с разворачиванием -массивов и ) и умеет:

  • list — таблица периферии: адрес, размер, IRQ;

  • enums — C-enum с базовыми адресами;

  • memmap — индексы *_IDX_* и массив DeviceConfig (то, что стало be_u1000_config[]);

  • registers — макросы REG32()/FIELD() из hw/core/registerfields.h плюс массив RegisterAccessInfo с масками ro/w1c/cor/rsvd и reset-значениями;

  • header — готовый .h для одного блока (include guard, SPDX, RSVD_MASK), для одинаковых блоков вроде ADC0/1/2 проверяет, что раскладка регистров совпадает.

Фильтры --group, --peripheral, --filter отбирают нужные блоки. Например, если взялись за WDT:

$ svd2qemu.py BE_U1000.svd list$ svd2qemu.py BE_U1000.svd header --peripheral WDT0 --guard HW_MISC_BE_U1000_WDT_H -o be-u1000-wdt.h

Регистровая часть нового устройства получается сразу, остаётся написать поведение. Надо помнить, что core-local блоки (ROM/TCM/SRAM/CLINT/CLIC) в SVD не описаны, так что в данном случае генератор не поможет.

Кросс-проверка

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

Например возьмём CLIC, для него:

через CSR управляется состояние ядра и обработка прерываний:

  • mtvec: режим CLIC (mode=0b11) и адрес общего обработчика для не векторных прерываний;

  • mtvt: база таблицы векторов для векторных (SHV) прерываний;

  • mcause: причина, а также mpil (уровень прерванного кода), mpp/mpie и minhv;

  • mintstatus (только чтение): текущий уровень прерывания mil;

  • mintthresh: порог, ниже которого прерывания маскируются;

  • mnxti: позволяет взять следующее pending-прерывание без выхода из обработчика (tail-chaining);

  • mstatus.MIE: глобальное разрешение прерываний;

  • mscratchcsw/mscratchcswl: смена стека при смене привилегий или уровня (если реализованы),

а через MMIO настройка отдельных источников:

  • cliccfg (BASE+0x0): nlbits, то есть сколько бит clicintctl уходит на уровень и сколько на приоритет (плюс nmbits для режима привилегий);

  • clicinfo (BASE+0x4, только чтение): число источников, версия, CLICINTCTLBITS;

  • clicinttrig[]: триггеры отладки (если реализованы);

  • на каждый IRQ i по 4 байта по адресу BASE+0x1000+4*i:

    • clicintip (+0): бит pending

    • clicintie (+1): бит enable

    • clicintattr (+2): shv (векторное или нет), trig (уровень/фронт, полярность), mode

    • clicintctl (+3): уровень и приоритет; на be-u1000 CLICINTCTLBITS=4, нижние 4 бита читаются как 1

C CSR всё понятно — они per-hart согласно спеке.

А вот с MMIO — спека однозначного ответа нам не даёт, там есть рекомендации, но они говорят, что CLIC управляется для каждого харта из общего MMIO региона, но со смещением.

В be-u1000 определен только один регион и тогда вывода может быть два:

  • регион shared и тогда всё что определено в MMIO региона влияет сразу на два hart’а (что мало вероятно);

  • они per-hart, то есть каждый hart читает и пишет, только свой собственный и не видит “соседнего”.

Тогда необходимо писать тест, который даст нам однозначный ответ, причем не обязательно на C или Asm, можно просто через cкрипты OpenOCD.

Так был сделан тест, чтобы проверить изоляцию CLIC, а заодно и CLINT MTIMER:

Подключение (добавить одну строку в свой EVU_BA_2_5.cfg, после source [find target/baikal/BE_U1000/registers.cfg]):

source [find target/baikal/BE_U1000/clic-isolation-tests.cfg]

Запуск из telnet-консоли OpenOCD, после обычной init-последовательности (targets CORE_0; init; halt; ___prepare_secondary_cores):

test_clic_isolationtest_clint_mtimecmp_isolation

Пример вывода test_clic_isolation:

=== CLIC_INTCTL_IRQ0 @ 0x0d001003 === (CORE_0) mhartid=0 OK (CORE_1) mhartid=1 OK (CORE_0) mhartid=0 OK (CORE_0) wrote 0xa0, reads back 0xaf (CORE_1) mhartid=1 OK (CORE_1) (after CORE_0's write) reads 0x0f (CORE_1) wrote 0x50, reads back 0x5f (CORE_0) mhartid=0 OK (CORE_0) (after CORE_1's write) reads 0xaf (CORE_0) mhartid=0 OK (CORE_1) mhartid=1 OK => VERDICT: register is PER-HART ISOLATED between CORE_0 and CORE_1=== end CLIC_INTCTL_IRQ0 ===

0xa0->0xaf и 0x50->0x5f — ожидаемо: у be-u1000 CLIC_INTCTLBITS=4, нижние 4 бита регистра аппаратно всегда 1.

Следовательно изоляция CLIC подтверждена и на реальном железе.

Готовый тест лежит в clic-isolation-tests.cfg.

Что можно сделать в рамках проекта

Ниже — периферия SoC, которую ещё предстоит сделать, сгруппированная по сложности, с примером из mcu-sdk-examples, который сразу пригодится для проверки. Адреса берутся из автогенерированного из SVD be_u1000_config[] (см. раздел про генератор выше), так что там, где есть реализация, адрес в QEMU совпадает со SVD. Все блоки SoC единым списком, вместе с уже готовыми, собраны в приложении в конце статьи.

Понятные первые задачи

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

Периферия

Статус

Что нужно сделать

Пример, на котором проверить

GPIO0–2

заглушка

регистровый банк set/clear + EXTI на CLIC

GPIO_LEDBlink (+C1/+C2), GPIO_InputPolling/IT/EXTI, Running_Lights (8 примеров)

WDT0/1

заглушка

счётчик + сброс харта через уже готовый core-reset механизм CRU

WDT_Reset

EFLASH

временный ROM-костыль

честная модель с контроллером (erase/program), сейчас заменена ROM-заглушкой только для загрузки

EFLASH_MemoryTest

-kernel для машины evu-ba

не подключён

завести riscv_load_kernel/обработку machine->kernel_filename в evu-ba.c, чтобы не нужен был -device loader вручную

удобство для всех примеров сразу

Средней сложности

Протокол/несколько режимов, но без внешних зависимостей:

Периферия

Статус

Комментарий

Примеры

TIM0/1, PWMG0/1

заглушка

базовый таймер + compare/capture/PWM

TIM_* (2), PWMG_* (4)

PWMA0–3 / QE0–3

заглушка

PWM + квадратурный энкодер, часть примеров завязана на DMA

PWMA_* (7)

I2C0–3, SPI0–3

заглушка

master/slave протокол

I2C_* (2), SPI_Master_Tx_DMA (1)

ADC0–2

заглушка

регистры + модель источника сигнала

ADC_* (11)

Mailbox (MB/MB1)

заглушка

есть дизайн generic mailbox (N views, FIFO с данными, конфигурируемые level/edge IRQ), реализация по datasheet ещё не начата.

нет примера — подходящая задача для тех, кто любит проектную работу с готовой спецификацией

Крупнее, со своими зависимостями

Периферия

Статус

Комментарий

Примеры

DMA0/1

заглушка

блокирует все _DMA-варианты примеров PWMA/ADC/I2S

DMA_* (3) + DMA-варианты внутри PWMA/ADC/I2S

I2S0/1

заглушка

аудио-поток, завязан на DMA

I2S_Slave_* (8)

QSPI0/1

заглушка

плюс XIP-режим

QSPI_Flash, QSPI_XIPLoader (2)

CANFD0/1

заглушка

арбитраж, кадры CAN FD

CANFD_OneBoard_Communication_Polling (1)

USB

заглушка

нет примера в SDK, нужно будет писать тесты с нуля

—

BM310 (CORE2): CLIC/MTIMER/адресное пространство

харт создаётся

завести cpu2_memory, свой CLIC и MTIMER по образцу CORE0/CORE1

нет примера под CORE_2 в текущем наборе — хороший повод завести

Инфраструктурные блоки (CORE2_INT/CORE2_PIO, CORE2_DEBUG/CORE2_TRACE) сейчас заглушки: функциональность они не блокируют, а может, и не нужны вовсе.

Ultra-Violence: MicroPython

MicroPython: тут я даже не думал с какой стороны подступиться и как это будет выглядеть в итоге.

Идеи для upstream

Две темы, которые выходят за рамки BE-U1000 и могли бы пригодиться всему QEMU.

Заметка про подсистему тактирования. В Linux для этого есть Common Clock Framework: гейты, делители, мультиплексоры и PLL описываются как узлы одного дерева, частота пересчитывается вниз по дереву автоматически, а драйвер контроллера только описывает топологию. В QEMU ничего подобного нет. Есть лишь базовый объект Clock (hw/core/clock.c, qdev-clock.c): входы и выходы тактирования у устройства, clock_set_hz()/clock_set_mul_div(), распространение частоты и колбэк на её изменение. Гейты, делители и мультиплексоры каждый контроллер пишет руками, а многие старые модели вообще считают частоты своими функциями мимо Clock.

При этом моделей контроллеров тактирования в QEMU уже немало. На Clock построены, например, BCM2835 CPRMAN (bcm2835_cprman.c, Raspberry Pi), STM32L4x5 RCC (stm32l4x5_rcc.c), Nuvoton NPCM7xx/8xx CLK (npcm_clk.c) и Zynq SLCR (zynq_slcr.c). Своим способом, без общего дерева, сделаны i.MX CCM (imx25/31/6/6ul/7/8mp_ccm.c), Allwinner CCM/CCU (allwinner-a10-ccm.c, allwinner-h3-ccu.c, allwinner-r40-ccu.c), Aspeed SCU (aspeed_scu.c), SiFive PRCI (sifive_e_prci.c, sifive_u_prci.c), Exynos4210 CLK, STM32 RCC (stm32_rcc.c), Axiado CLK и OMAP. Теперь к ним добавляется наш CRU для BE-U1000 со своими GATE/DIV/MUX/PLL. Каждая из этих моделей заново решает одни и те же задачи, так что общий слой в духе CCF (типовые узлы gate/divider/mux/pll поверх Clock и описание дерева таблицей) уже напрашивается. Это хорошая идея для upstream.

Заметка про PINMUX. В QEMU мультиплексирование выводов по сути бессмысленно: у эмулируемой машины нет физических ножек, а устройства подключены к внешнему миру напрямую через бэкенды (UART — к chardev, CAN — к can-bus, сеть — к netdev). UART отдаст байт в свой chardev независимо от того, на какую ножку его TX вывела прошивка. Поэтому обычно достаточно, чтобы регистры выбора альтернативной функции читались и писались без последствий — HAL их инициализирует и идёт дальше. Смысл появляется, только если моделировать плату целиком: светодиод, кнопку или датчик на конкретной ножке, или ловить ошибки конфигурации, когда прошивка забыла переключить ножку в нужную функцию, и на железе ничего бы не заработало.

Для STM32, например, уже была попытка реализовать нечто подобное (beckus/qemu_stm32), но общего решения в QEMU нет: каждая машина делает по-своему или не делает вовсе. Идея открытая: универсальный фреймворк pinmux/pinctrl для QEMU тоже был бы интересной задачей сразу для upstream, так что если кто-то за него возьмётся — будем рады помочь.

Пойдет ли данная работа в upstream

BE-U1000 делает «Байкал», компания под санкциями, и реакция мейнтейнеров на подобные патчи уже известна — в лучшем случае ответят «Consult your lawyers» (см. историю с e2k — ссылка), в худшем просто промолчат. Тратить время на переписку, чтобы услышать «юрист вам в помощь», смысла нет.

Поэтому работа пойдёт в форк LANDAU QEMU, и мы её будем поддерживать. Это просто рабочая необходимость, раз upstream закрыт по причинам, не связанным с кодом, инструмент должен жить в другом месте — всё, что сделано и будет сделано по BE-U1000 в форке.

Осталось почти всё — но то, что осталось, теперь можно есть по кусочку. Присоединяйтесь.

Приложение: полная таблица периферии (SVD → код QEMU → пример в mcu-sdk-examples)

Все блоки SoC из автогенерированного из SVD be_u1000_config[] (hw/riscv/be-u1000.c), со статусом в QEMU, оценкой сложности реализации и примером в SDK. Везде, где в коде есть реальная реализация, адрес в QEMU совпадает со SVD без расхождений.

Периферия

Статус в QEMU

Оценка сложности реализации

Пример в SDK

CRU (тактирование)

готово

сделано

нет отдельного — используется неявно во всех

Reset Controller (в составе CRU)

готово

сделано

нет отдельного

CLIC (CORE0/CORE1)

готово

сделано, draft 0.10

CLIC_NestedInterrupts, CLIC_Vectored (2)

CLINT (CORE2/BM310)

не начато

высокая (своё приватное адресное пространство BM310 + подключение)

—

MTIMER (CORE0/CORE1)

готово

сделано

нет отдельного (используется неявно)

MTIMER (CORE2/BM310)

не начато

средняя (переиспользовать riscv_mtimer, нужен 3-й view + память BM310)

—

ROM0/ROM1

готово (изолированы по хартам)

сделано

нет отдельного

TCMA/TCMB

готово

сделано

нет отдельного

SRAM

готово

сделано: resetvec CORE0/CORE1 на EFLASH, SRAM создаётся и маппируется

используется как RAM почти всеми примерами

EFLASH + EFLASH_CONTROLLER

временный ROM вместо честной модели

низкая (erase/program протокол + контроллер)

EFLASH_MemoryTest

UART0–7

базовая имплементация

сложность низкая, но очень нудно регистров много функциональности тоже много, часть нельзя реализовать в QEMU.

UART_printf, UART_printf_bootROM, UART_OneBoard_Com* (5)

-kernel для evu-ba

не подключён

низкая (riscv_load_kernel/machine->kernel_filename)

удобство для всех примеров

GPIO0–2

заглушка

низкая (регистровый банк set/clear + EXTI на CLIC)

GPIO_LEDBlink(+C1/+C2), GPIO_InputPolling/IT/EXTI, GPIO_InputPollingBSP, Running_Lights (8)

WDT0/1

заглушка

низкая (счётчик + сброс харта через core-reset CRU)

WDT_Reset (1)

TIM0/1

заглушка

средняя (base timer, compare/capture)

TIM_TimeBase, TIM_PWMOutput (2)

PWMG0/1

заглушка

средняя (как TIM + PWM-выход/захват)

PWMG_TimeBase/PWMOutput/InputCapture/OutputCompare (4)

PWMA0–3 / QE0–3

заглушка

средняя-высокая (PWM + квадратурный энкодер + DMA-burst режимы)

PWMA_* (7, в т.ч. с DMA)

I2C0–3

заглушка

средняя (master/slave протокол)

I2C_BusPolling, I2C_OneBoard_Communication_* (2)

SPI0–3

заглушка

средняя

SPI_Master_Tx_DMA (1)

CANFD0/1

заглушка

высокая (арбитраж, кадры CAN FD)

CANFD_OneBoard_Communication_Polling (1)

I2S0/1

заглушка

средняя-высокая (аудио-поток, завязан на DMA)

I2S_Slave_* (8)

QSPI0/1

заглушка

средняя-высокая (плюс XIP-режим)

QSPI_Flash, QSPI_XIPLoader (2)

ADC0–2

заглушка

средняя (регистры + модель источника сигнала)

ADC_* (11)

DMA0/1

заглушка

высокая (блокирует все _DMA-примеры)

DMA_MemoryToMemory_SRAM/TCM, DMA_MemoryToPeriph (3, + DMA-варианты PWMA/ADC/I2S)

USB

заглушка

высокая

нет примера

MB/MB1 (mailbox)

заглушка, есть дизайн

средняя (дизайн готов, реализация не начата)

нет примера

AXI/AHB, CORE2_INT/CORE2_PIO, CORE2_DEBUG/CORE2_TRACE

заглушки

низкая (debug/bus-инфраструктура, не блокирует функциональность)

нет примера

MicroPython

0

запредельная

нет примера

Источник: habr.com

0 0 голоса
Рейтинг новости
1
0
Подписаться
Уведомить о
0 комментариев