Ровно полгода назад, 18 августа 2025 года, я опубликовал здесь свою первую статью о портировании прошивки AM32 на отечественный микроконтроллер К1946ВК035.

Ссылка на статью - https://yg140.servegame.com/ru/articles/938128/

Те, кто читал ту статью (а таких людей, уверен, немного), помнят: не весь функционал удалось портировать с сохранением исходной производительности из-за некоторых ограничений в работе периферийных модулей отечественного микроконтроллера.
Напомню суть проблемы: микроконтроллер слишком часто уходит в прерывания для обработки входящих сигналов DSHOT, которые мы пытались обрабатывать сугубо софтварно, без применения DMA (но с небольшими хитростями). Отсюда и проблемы со своевременной обработкой сигналов других частей программы.

Для всех неравнодушных — прошу пожаловать под кат.

При более детальном изучении datasheet на данный микроконтроллер можно понять, что да, у модуля ECAP, который предназначен для измерения длительности логических нуля и единицы у входящих сигналов, нет DMA, хоть и есть небольшой входной буфер, которого недостаточно. Но у нас есть модуль GPIO, который подключен к DMA, а значит можно триггерить при изменении уровней сигнала другие блоки микроконтроллера, например внутренние таймеры, с помощью которых можно измерять время от одного события до другого, а значит — измерять длительность импульсов. Тут я думаю, вы уже понимаете, к чему я клоню. Мы виртуально создадим нужный нам функционал в микроконтроллере, который обычно реализуется определенным режимом таймера у таких микроконтроллеров, как STM32 (input capture), только задействуем для этого GPIO + DMA + внутренний таймер.

Сам с��гнал, который нам предстоит парсить, выглядит обычно вот так:

Это последовательность из 16 импульсов (16 бит), чем-то напоминающая классический ШИМ, но разной длительности.Итак, давайте посмотрим, как может выглядеть код, с помощью которого мы можем сделать то, что мы задумали.

    RCU->HCLKCFG_bit.GPIOAEN = 1; //Включаем тактирование шины GPIOA
    RCU->HRSTCFG_bit.GPIOAEN = 1; //Включаем тактирование шины GPIOA
    GPIO_LockKeyCmd(GPIOA, ENABLE); //Снимаем LOCK(об этом позже)
    GPIOA->ALTFUNCCLR_bit.PIN5 = 1; //Убираем режим альтернативной функции
    GPIOA->LOCKCLR_bit.PIN5 = 1; //Снимаем LOCK с отдельного пина
    GPIOA->DENSET_bit.PIN5 = 1; //Включаем цифровую функцию пина
    GPIOA->DMAREQSET_bit.PIN5 = 1; //Включение запросов DMA
    GPIOA->INTTYPESET_bit.PIN5 = 1; //Включаем прерывания по фронту сигнала
    GPIOA->INTEDGESET_bit.PIN5 = 1; //Включаем события по фронту и спаду

Наш пин на шине GPIOA триггерит DMA, не вызывая при этом прерываний.

Полдела уже сделано, а теперь — как и обещал — насчёт функции LOCK.Так уж вышло, что при запуске микроконтроллера, когда все регистры должны быть сброшены в 0x00 (ну или почти все), на шине GPIOA есть парочка, которые инициализируются не нулями, а 0x7C. Отсюда получается, что часть пинов шины GPIOA недоступны сразу после загрузки микроконтроллера, а именно пины со 2 по 6, а, как уже понятно из вышеизложенной программы, наш пин — пятый. Чтобы начать работать с этим пином и иметь возможность менять его конфигурацию, надо его разблокировать. Сделать это можно с помощью специальной функции GPIO_LockKeyCmd из plib035 (peripheral library) из SDK, доступного на GitFlic.

Настроим таймер:

    RCU->PCLKCFG_bit.TMR3EN = 1; //Включаем тайтирование таймера
    RCU->PRSTCFG_bit.TMR3EN = 1; //Включаем тайтирование таймера
    TMR3->VALUE = 0xFFFFFFFF; //Инициализируем начальное значение счёта
    TMR3->LOAD = 0xFFFFFFFF; //Инициализируем значение после перезагрузки
    TMR3->CTRL_bit.ON = 1; //Включаем счёт

Кого смутила инициализация таймера числом 0xFFFFFFFF, поясню: таймеры у ВК035 считают вниз.

Настроим DMA на передачу значения счётчика таймера в буфер по запросу от GPIOA.

    DMA->BASEPTR = (uint32_t)(&DMA_CONFIGDATA); //передаём ссылку на конфиг DMA
    DMA->ENSET_bit.CH8 = 1; //Включаем канала DMA 1
    DMA_CONFIGDATA.PRM_DATA.CH[8].SRC_DATA_END_PTR = (uint32_t)(&TMR3->VALUE); //Адрес источника данных 
    DMA_CONFIGDATA.PRM_DATA.CH[8].CHANNEL_CFG_bit.SRC_SIZE = DMA_CHANNEL_CFG_SRC_SIZE_Word; //Разрядность данных источника
    DMA_CONFIGDATA.PRM_DATA.CH[8].CHANNEL_CFG_bit.SRC_INC =  DMA_CHANNEL_CFG_SRC_INC_None; // Не инкрементируем
    /* приемник */
    DMA_CONFIGDATA.PRM_DATA.CH[8].DST_DATA_END_PTR = (uint32_t )&(rawBuffer[32-1]); //Адрес конца данных приемника
    DMA_CONFIGDATA.PRM_DATA.CH[8].CHANNEL_CFG_bit.DST_SIZE = DMA_CHANNEL_CFG_SRC_SIZE_Word; //Разрядность данных приемника
    DMA_CONFIGDATA.PRM_DATA.CH[8].CHANNEL_CFG_bit.DST_INC = DMA_CHANNEL_CFG_DST_INC_Word; //Инкрементируем на байт
    DMA_CONFIGDATA.PRM_DATA.CH[8].CHANNEL_CFG_bit.R_POWER = 0x0; // Количество передач до переарбитрации
    DMA_CONFIGDATA.PRM_DATA.CH[8].CHANNEL_CFG_bit.N_MINUS_1 = 32 - 1; //Общее количество передач DMA
    DMA_CONFIGDATA.PRM_DATA.CH[8].CHANNEL_CFG_bit.CYCLE_CTRL = DMA_CHANNEL_CFG_CYCLE_CTRL_Basic; //Задание типа цикла DMA 

    DMA_ChannelMuxConfig(DMA_ChannelMux_8, DMA_ChannelMux_8_GPIOA); //Настройка микшера DMA, 8 канал можно настроить не только на GPIO

Из необычного: в структуру настройки DMA передаются адрес на конец буфера для передачи данных, а не начало.

Тип цикла DMA у нас BASIC, т.е. DMA закончит свою работу после 32 передач (а именно такого размера буфер нам и нужен, ведь количество "фронтов" сигнала у нас 32). Мы вынуждены будем заново запускать DMA после окончания передачи. Это сделано потому, что в программе нам нужно будет переключать линию на передачу — ведь мы хотим реализовать двухсторонний DSHOT и отправку данных телеметрии по этой же линии обратно на полётный контроллер для реализации такого функционала, как RPM Filtering.

По итогу, когда DMA завершит свою работу, мы получим прерывание, в котором сначала приведём значения счётчика таймера к более привычному виду, а именно — 0xFFFFFFFF минус значение счётчика , т.к. таймер у нас считает вниз. А потом уже будем обрабатывать. В чётных ячейках буфера мы будем иметь длительность логической единицы нашей посылки DSHOT, а в нечётных — длительность логического нуля.

Всю проделанную работу с прошивкой можно посмотреть всё по той же ссылке из прошлой статьи. А подробности про реализацию двухстороннего DSHOT можно будет узнать в следующей статье, если конечно эта будет кому-то интересна.