TOPZNANIE

Цифровая практика без сложностей

Выберите город
MET Norway · CC BY 4.0
← На главную

· Общий обзор

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-битных АЦП и блоки для управления электродвигателями, а также Микро-Питон (что бы это ни значило).

Именно для него модель в 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.

Архитектура модели (что подключено к чему)

LANDAU: Мы сделали 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) # с системным picolib mcu-sdk-examples $ PICOLIBC=local make -j$(nproc) # с локальным picolib mcu-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:stdio UART printf example UART printf example UART printf example UART printf example UART 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:stdio UART printf example UART printf example UART printf example UART 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) load Loading section .text, size 0x4ab6 lma 0xa0000000 Loading section .rodata, size 0x8df lma 0xa0004ab8 Loading section .data, size 0x14 lma 0xa0005398 Start address 0xa0000030, load size 21417 Transfer rate: 171336 bits in <1 sec, 7139 bytes/write. (gdb) c Continuing. […]

Как видите здесь выбор 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.log memory_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.elf Semihosting printf example Semihosting printf example Semihosting printf example Semihosting 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

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

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

  • 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_isolation test_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

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

нет примера

Теги:

  • qemu
  • be-u1000
  • risc-v

Хабы:

  • Виртуализация
  • Системное программирование
  • C
  • Open source