RISCованный полёт: порт AM32 на К1921ВГ5Т


В предыдущих сериях мы портировали прошивку AM32 на отечественный микроконтроллер К1946ВК035 и пытались заставить его дружить с DShot.
Первая статья — порт AM32 на К1946ВК035.
Вторая — продолжение про DShot.
Те, кто читал предыдущие статьи, помнят: основная борьба шла не с вычислительной мощностью микроконтроллера, а с особенностями его периферии. В частности, модуль ECAP у ВК035 не умеет напрямую отправлять запросы DMA. В итоге для приёма DShot пришлось создавать свой «input capture» из GPIO, DMA и таймера.
В этот раз на операционном столе оказался другой отечественный микроконтроллер — К1921ВГ5Т от НИИЭТ. И здесь нас ждёт новое ядро, другой контроллер прерываний и несколько приятных сюрпризов в работе периферии.
На данный момент прошивка уже выполняет свою основную задачу: принимает управляющий PWM-сигнал, запускает бесколлекторный двигатель и управляет им в sensorless closed loop. DShot, телеметрию и другие возможности будем добавлять по мере тестирования.
Исходники проекта — на GitHub.
А для остальных — прошу под кат.

Фото платы ESC с К1921ВГ5Т . Ну а куда же без навесного монтажа и МГТФ?
Дисклеймер: всё ниже — авторские догадки и наблюдения. Автор может ошибаться и, вполне возможно, пока не до конца понимает, о чём пишет. Разумные поправки приветствуются.
Внимательный читатель, возможно, заметит на фотографии несколько проводков, припаянных прямо к выводам микроконтроллера. Это не новая технология разводки, а результат того, что автор недостаточно внимательно прочитал документацию перед изготовлением платы.
После работы со STM32 и ВК035 я привычно предусмотрел подключение отладчика по SWD. А для отладки ВГ5Т в этом проекте используется JTAG — со своими линиями TCK, TMS, TDI и TDO. В итоге недостающие соединения пришлось припаивать прямо к выводам микроконтроллера. Какие именно линии потребуются, включая сброс, нужно проверить по схеме подключения выбранного отладчика и документации на МК.
Не будьте как автор: читайте документацию до разводки платы, а не после того, как плата уже приехала.
Новый микроконтроллер — новая прошивка?
Прошивку удобно разделить на две части. Основной код содержит логику управления мотором: запуск, коммутацию, обработку команд и расчёты. Платформенная часть связывает эту логику с конкретным микроконтроллером — настраивает PWM, таймеры, GPIO, АЦП и прерывания.
При добавлении нового МК основные алгоритмы можно сохранить, а вот с периферией приходится разбираться заново. Поэтому datasheet всё равно остаётся настольной книгой.
ВК035 построен на Cortex-M4F, а ВГ5Т — на RISC-V. Так что в этот раз нам предстоит познакомиться не только с другим набором регистров, но и с другой системой обработки прерываний.
Есть ещё одно важное отличие — тактирование. В порте для ВК035 PLL получала опорный сигнал от внутреннего 8-МГц генератора (в документации ВК035 он называется OSI; по привычке его часто называют HSI). У ВГ5Т внутренний HSI работает на 4 МГц, а вход PLL выбирается только между HSE и внешним тактовым сигналом. Кроме того, в используемой PLIB для опорной частоты PLL задан минимум 10 МГц. У PLL есть множитель FBDIV (в PLIB — от 16 до 160), делитель опорного сигнала RDIV и два выходных делителя DIV1A и DIV1B (каждый настраивается от 1 до 8). Но эти коэффициенты не помогают: HSI не подключён ко входу PLL и сам по себе даёт только 4 МГц системной частоты. Для прошивки нужен внешний кварц HSE на 16 МГц.
В проекте из этих 16 МГц PLL формирует системные 96 МГц. Используются обратный делитель NDIV = 96, опорный делитель RDIV + 1 = 2 и выходные делители DIV1A + 1 = 4 и DIV1B + 1 = 2:
F_SYSCLK = 16 МГц × 96 / (2 × 4 × 2) = 96 МГцДля сравнения: в текущей настройке PWM счётчик получает 48 МГц после деления системной частоты на два, а период задаёт выходную частоту 24 кГц. На ВК035 внутренние 8 МГц можно было подать на PLL; у ВГ5Т HSI в четыре раза медленнее и не подключён ко входу PLL. Поэтому здесь без HSE нужный режим тактирования не собрать.
PWM: старый знакомый
В оригинальных сборках AM32 для STM32 часто используется advanced timer, например TIM1. В одном блоке он умеет формировать комплементарные сигналы, задавать dead-time и помогать с синхронизацией АЦП.
У ВК035 и ВГ5Т подход другой: для трёх фаз используются три отдельных PWM-блока. У каждого есть свой счётчик, регистры сравнения и настройка dead-time.
С этой частью ВГ5Т особых сюрпризов после ВК035 не преподнёс. Периферия знакома по общей идее: настраиваем три PWM-модуля, задаём заполнение и задержку между переключением ключей. При коммутации выбираем, какая фаза сейчас управляется, какая подключена к земле, а какая остаётся свободной для измерения обратной ЭДС.
Аппаратные события и триггеры
В прошивке ESC время дорого. Пока процессор обрабатывает одно событие, мотор не останавливается и вежливо его не ждёт.
Поэтому хороший микроконтроллер для этой задачи должен не только быстро выполнять код, но и позволять периферии делать часть работы самостоятельно.
В этой ESC полезны прежде всего две аппаратные цепочки. Первая привязывает измерение АЦП к PWM: PWM0 формирует событие SOCA в выбранной точке периода, а секвенсор АЦП выбирает PWM0A как источник запуска. Так измерение можно синхронизировать с переключением силовых ключей, не заводя отдельное прерывание ради старта каждого преобразования.
Вторая цепочка нужна для будущего приёмника DShot: CAP фиксирует фронты входного сигнала и может сформировать запрос DMA; DMA переносит значения регистров захвата в RAM. В текущей версии прошивки этот путь не включён — здесь он приводится как возможность периферии, а не как реализованный DShot.
В PLIB из SDK НИИЭТ перечислены и другие источники DMA: SPI0/1, UART0/1, TMR0–3, ADCSEQ0/1, GPIOA/B, PWM0–2, QEP и CAP0–2. Это список аппаратных requestor-ов контроллера, а не обещание, что каждый блок может запускать любую передачу в любой конфигурации. Для этой статьи достаточно нескольких практических примеров: PWM → старт АЦП, CAP → DMA для входных фронтов, UART/SPI → DMA для приёма или выдачи потоковых данных. Перечень периферии самого микроконтроллера опубликован также на странице К1921ВГ5Т у НИИЭТ.
Связь PWM → АЦП в проекте задаётся с обеих сторон: PWM0 выдаёт импульс SOCA при обнулении счётчика, а секвенсор 0 принимает PWM0A как внешний источник запуска:
// PWM0: сформировать SOCA при событии CTR == Zero.
PWM0->ETSEL_bit.SOCASEL = PWM_ETSEL_INTSEL_CTREqZero;
PWM0->ETPS_bit.SOCAPRD = 1;
PWM0->ETSEL_bit.SOCAEN = 1;
// ADC: запускать секвенсор 0 от сигнала PWM0A.
ADC->EMUX_bit.EM0 = ADC_EMUX_EM0_PWM0A;
ADC->SEQEN_bit.SEQEN0 = 1;Это сокращённый фрагмент связки, а не полная инициализация АЦП: тактирование, выбор входного канала, калибровка и чтение результата настраиваются отдельно. Именно такая связка используется в PWM_TIM1_Init() и ADCInit().
CAP и DMA: наконец-то встретились
А вот здесь мы подошли к моей личной боли из предыдущих прошивок. Те, кто помнит прошлую статью, уже наверняка догадались, про какой именно блок сейчас пойдёт речь.
На ВК035 модуль ECAP умел захватывать входной сигнал, но напрямую запросить DMA не мог. Поэтому сначала мы обрабатывали захват в прерываниях, а затем придумали обходной путь через GPIO, DMA и таймер.
Во ВГ5Т у модулей CAP0, CAP1 и CAP2 уже есть собственные источники запросов DMA. Причём запрос можно сформировать по событиям захвата CEVT0–CEVT3, а также по некоторым событиям счётчика.
Для будущей реализации DShot это открывает более прямой путь:
Входной сигнал
↓
CAP захватывает событие
↓
CAP формирует запрос DMA
↓
DMA переносит данные в буфер
↓
Процессор обрабатывает готовые данныеПримерно так может выглядеть настройка источника DMA-запроса и описания передачи:
uint32_t capture_buffer[16];
DMA_ChannelInit_TypeDef dma;
DMA_ChannelStructInit(&dma);
dma.SrcDataPtr = (void *)&CAP0->CAPREG0;
dma.DstDataPtr = capture_buffer;
dma.BytesTotal = sizeof(capture_buffer);
dma.SrcReqest = DMA_Request_CAP0;
dma.DstReqest = DMA_Request_Memory;
dma.SrcDataSize = DMA_DataSize_32;
dma.DstDataSize = DMA_DataSize_32;
dma.SrcDataInc = DISABLE;
dma.DstDataInc = ENABLE;
dma.LastDescr = ENABLE;
dma.InterruptEnable = ENABLE;
// CAP0 формирует RX-запрос DMA при событии захвата 0
CAP0->DMARXREQ_bit.CEVT0 = 1;Здесь источником указан один регистр захвата, а приёмником — буфер в RAM. SrcDataInc оставляет адрес регистра постоянным, DstDataInc продвигает адрес в буфере. В PLIB для ВГ5Т размер линейной передачи задаётся количеством байт (BytesTotal, от 1 до 1024). В настройке DMA для ВК035 конечный адрес буфера задавался отдельно: например, DST_DATA_END_PTR = &rawBuffer[32 - 1], а число передач — через N_MINUS_1. Для массива uint32_t capture_buffer[16] в ВГ5Т указывают sizeof(capture_buffer), то есть 64 байта.
У DMA есть дескрипторная цепочка: NextDescrPtr задаёт адрес следующего дескриптора, а LastDescr говорит остановиться после текущего буфера или перейти к следующему по NEXT_ADDR. Для циклического режима комментарий в SDK предлагает указать адрес регистра SRC_PTR текущего канала. Это адрес для повторной загрузки команды, а не «адрес конца буфера». В этом месте архитектура отличается от привычного по ВК035 способа описания передачи; для ВГ5Т размер и инкремент адресов задаются отдельно.
Это заготовка, а не готовый приёмник DShot: в ней опущены тактирование, выбор входного вывода и фронтов CAP, настройка и запуск канала DMA. Кроме того, CAP имеет четыре регистра захвата: прежде чем применять такой вариант, нужно проверить последовательность событий и чтения регистров, направление запроса и работу с буфером на железе. Настройка только CEVT0 не означает автоматический захват каждого фронта пакета.
То есть идея, ради которой на ВК035 пришлось подружить несколько разных блоков, здесь предусмотрена в самой периферии.
PLIC вместо NVIC: когда привычное работает иначе
На STM32 и ВК035 мы привыкли к NVIC: периферия сообщает о событии, а контроллер прерываний передаёт управление нужному обработчику.
На ВГ5Т внешние прерывания проходят через PLIC — Platform-Level Interrupt Controller. Упрощённо путь события выглядит так:
Событие периферии
↓
PLIC отмечает запрос
↓
Ядро получает внешнее прерывание
↓
Общий обработчик PLIC определяет источник
↓
Вызывается обработчик периферии
↓
Очистка флага и завершение запроса в PLIC
↓
Возврат к основной программеНапример, обработчик таймера коммутации в проекте выглядит так:
__RAMFUNC void COM_TIMER_IRQHandler(void)
{
COM_TIMER->IM_bit.TMR = 0; // Однократный запрос; следующий включит планировщик
COM_TIMER->IC = TMR_IC_TMR_Msk;
PeriodElapsedCallback();
}До вызова этого обработчика общий обработчик определяет источник через PLIC, а после — сообщает контроллеру о завершении обработки. Это часть пути прерывания, даже если сам обработчик периферии занимает всего несколько строк.
Путь в проекте можно проследить по настоящим именам. TENKHz_SysTick_Init() назначает PLIC_MachHandler обработчиком машинного внешнего прерывания через riscv_irq_set_handler(RISCV_IRQ_MEI, PLIC_MachHandler). В PlicIrqSetup() вызовы PLIC_SetIrqHandler() связывают номера источников PLIC с обработчиками COM_TIMER_IRQHandler, GPIOB_IRQHandler и ADC_SEQ0_IRQHandler.
Дальше PLIC_MachHandler() получает номер активного источника через внутреннюю PLIC_ClaimIrq(), вызывает зарегистрированную функцию из таблицы mach_plic_handler[], а затем завершает запрос через PLIC_ClaimComplete(). Например, для обратной ЭДС цепочка заканчивается в GPIOB_IRQHandler(), а для таймера коммутации — в COM_TIMER_IRQHandler(). При этом сам обработчик периферии очищает флаг своего блока: это отдельная операция от завершения запроса в PLIC.
Если свести это к схеме, получится так:
GPIOB или TMR1 выставляет запрос
↓
RISCV_IRQ_MEI → PLIC_MachHandler()
↓
PLIC_ClaimIrq() возвращает номер источника
↓
mach_plic_handler[номер]() вызывает GPIOB_IRQHandler()
или COM_TIMER_IRQHandler()
↓
Обработчик очищает флаг GPIOB/TMR1
↓
PLIC_ClaimComplete() завершает запрос в PLICИмена приведены из текущего кода проекта; для АЦП аналогично зарегистрирован ADC_SEQ0_IRQHandler().
На каждом из этих этапов тратится время процессора. Нужно сохранить контекст, определить источник, вызвать нужную функцию, обработать событие и вернуться обратно.
Размещение входного кода прерывания в TCM уменьшает задержки доступа к инструкциям и делает время реакции менее зависимым от состояния кэша. Для коротких критичных обработчиков это помогает получить более предсказуемый отклик.
Если отправлять процессор в прерывание на каждый фронт быстрого сигнала, он очень скоро начнёт выполнять только одну задачу: ходить в прерывания. Мотору такая многозадачность может не понравиться.
Поэтому правило здесь простое: всё, что можно поручить периферии, стараемся поручить периферии. PWM формирует сигнал сам, ADC запускается по событию, DMA переносит данные в память, а процессор подключается там, где действительно нужно принять решение.
В текущей прошивке наиболее важные события — коммутация и обработка обратной ЭДС — получают более высокий приоритет, чем измерение напряжения батареи.
Вход управления и обратная связь
Сейчас регулятор принимает обычный PWM-сигнал. Тайер TMR0 фиксирует фронт и спад импульса, а прошивка определяет длительность по разнице захваченных значений. CAP опрашивается из 20-кГц процедуры; отдельного CAP-прерывания и DMA в этой версии нет.
У PA14 из внутренних режимов доступен только pull-up; pull-down в GPIO ВГ5Т нет. Поэтому для PWM-входа нужен внешний резистор pull-down на землю: он задаёт низкий уровень, когда сигнал отключён, и не даёт входу ловить ложные срабатывания. Это подтверждается описанием однобитного PULLMODE в заголовке МК и enum GPIO_PullMode_TypeDef в SDK НИИЭТ: перечислены только Disable и PU.
Для обратной связи применяются внешние компараторы, как и в порте для ВК035. Их выходы подключены к GPIO ВГ5Т. После каждой коммутации прошивка выбирает свободную фазу и ожидает нужный фронт сигнала компаратора.
Когда переход через ноль обнаружен, таймер помогает определить момент следующей коммутации. Так мотор после запуска переходит к работе в closed loop.
Здесь архитектура получилась близкой к предыдущему порту. Компараторы выполняют аналоговую часть работы, а микроконтроллеру остаётся вовремя обработать цифровое событие.
Watchdog и потеря PWM
В обычной сборке включён watchdog с тактированием от HSE 16 МГц и LOAD=3200000: первый интервал составляет около 200 мс, следующий — ещё около 200 мс до системного сброса. Основной цикл регулярно сбрасывает счётчик, а delayMillis() обслуживает его во время стартовых и arming-сигналов. Потеря входного PWM отдельно обрабатывается логикой AM32 и приводит к системному сбросу. Обычная Debug-сборка также сохраняет watchdog и системный сброс, поэтому поведение при запуске из VS Code совпадает с рабочей прошивкой. При перезагрузке микроконтроллера отладочная сессия отваливается и для продолжения её приходится запускать заново.
Для отладки предусмотрен отдельный флаг VG5T_DEBUG_NO_RESET. При ON watchdog не запускается и не обслуживается, а вызовы системного сброса при потере сигнала заменяются очисткой тайм-аута. Это позволяет не терять отладочную сессию из-за такого сброса. По умолчанию флаг выключен. Например, можно собрать отдельный вариант так:
cmake -S . -B build-debug-no-reset -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=cmake/riscv-none-elf.cmake \
-DCMAKE_BUILD_TYPE=Debug -DUSE_BOOTLOADER=OFF \
-DVG5T_DEBUG_NO_RESET=ON
cmake --build build-debug-no-resetSDK и OpenOCD: портируем ещё и инструменты
В использованной версии SDK пришлось поправить описание Flash и GPIO, работу масок таймерных прерываний, параметры DMA, а также несколько имён регистров CAN и machine timer. Для самого проекта настроили тактирование и адрес приложения: 0x0000 без загрузчика или 0x1000 с загрузчиком. SVD-файл добавили, чтобы отладчик показывал имена регистров.
OpenOCD не проходил проверку Flash: сборка ожидала CHIPID 0x04e4c400, а микроконтроллер сообщал 0x04e4c300.
CHIPID error, CHIPID=4e4c300
Error: auto_probe failedВ Windows через Zadig заменяют драйвер BULK interface (Interface 2) J-Link на WinUSB, чтобы OpenOCD мог обращаться к адаптеру. Драйвер Interface 0 (USB-UART) менять не нужно.
При чтении буфера в окне памяти отладчик мог зависнуть или дождаться тайм-аута. Штатная конфигурация OpenOCD отводила под свою рабочую область все 16 КБ RAM, включая память прошивки. Область перенесли в свободное место и увеличили тайм-ауты:
K1921VG5T.cpu configure -work-area-phys 0x20000800 \
-work-area-size 0x2000 -work-area-backup 0
riscv set_command_timeout_sec 30
riscv set_reset_timeout_sec 30Настройка подключена в launch.json; адрес 0x20000800 выбран между данными программы и стеком.
Для FPU ядра выбрали ABI ilp32f: с ilp32d GDB сообщал о несовпадении разрядности регистров.
Сборка и отладка
Сборка настраивается через npm и xPack. Установите Node.js 22 LTS или новее, откройте каталог проекта в терминале и выполните:
npm install
npm run configure
npm run buildДля Debug-сборки используются отдельные команды и каталог build-debug:
npm run configure:debug
npm run build:debugnpm install устанавливает xpm, который загружает RISC-V GCC/GDB, CMake и Ninja. Для прошивки и отладки через J-Link отдельно понадобится OpenOCD НИИЭТ с поддержкой VG5T и скрипты из SDK. Конфигурации VS Code для J-Link находятся в .vscode/launch.json; детали установки OpenOCD и Zadig приведены в README проекта.
Для текущих испытаний приложение собирается без загрузчика и начинается с адреса 0x0000. Сборка с USE_BOOTLOADER=ON сохраняется как вариант: она оставляет первые 4 КБ под загрузчик и начинает приложение с 0x1000. Исходный код загрузчика в репозиторий не входит.
Итог
К1921ВГ5Т оказался интересным продолжением работы над портом AM32. После ВК035 силовая часть и PWM выглядят достаточно знакомо, зато RISC-V, PLIC и новые возможности DMA добавляют поводов снова открыть документацию.
Особенно порадовала связка CAP с DMA. На ВК035 ради обработки DShot пришлось придумывать обходное решение, а здесь периферия уже предлагает более прямой путь. Осталось проверить его на практике — и, возможно, для следующей статьи опять придётся доставать осциллограф.
Регулятор уже работает с мотором в closed loop. После последней прошивки владелец подтвердил, что мотор больше не срывается в испытанном режиме. DShot, телеметрия и другие функции будут появляться по мере тестирования.
Список источников и материалов
К1921ВГ5Т — страница производителя НИИЭТ с техническими материалами.
K1921VG5T SDK НИИЭТ — заголовки, PLIB и OpenOCD-скрипты.
OpenOCD НИИЭТ и релизы — драйвер Flash для микроконтроллеров НИИЭТ.
Краткое руководство НИИЭТ по RISC-V — установка WinUSB на
BULK Interface (Interface 2)в Zadig.RISC-V Privileged Architecture — машинные прерывания, trap CSRs и счётчик
mcycle.K19XXVK035_AM32 — предыдущий авторский порт.
Руководство пользователя К1921ВК035 — внутренний генератор OSI и PLL.
Фрагменты проекта и подписи к схемам — авторские или взяты из исходников этого порта. Названия регистров и периферийные возможности сверяются по SDK и документации НИИЭТ. У включённых SDK-файлов сохранены уведомления правообладателя; лицензия корневого проекта не заменяет условия SDK.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.