Получаем удовольствие от дешевых китайских микроконтроллеров (CH32V003)

от автора

Если вы оказались здесь, то скорее всего помните как в еще в 2022 году одним из самых важных событий в мире (DIY) была новость про микроконтроллер за 10 центов от уже известной всему миру благодаря своему USB-UART свистку CH340 компании Nanjing Qinheng Microelectronics Co., Ltd, далее WCH.

Отладку от самой WCH, плату от WeAct и даже сами камни я заказал на Али, потыкал в пару примеров и забыл. Для DIY-проектов мне гораздо больше понравились платы от WeAct с ch32x035 и ch32v203, по стоимости примерно такие же, а функционала сильно больше, но в этом году на просторах китайского маркетплейса мне стала попадаться плата с героем статьи, да еще и с USB-C на ней.

Она стоит заметно дешевле своих собратьев и на момент заказа мне обошлась за 90 рублей в сумме с доставкой, а значит, новому королю DIY — быть.

Так и родилась идея сделать свой sdk.

дешевая платка с ch32v003f4p4

дешевая платка с ch32v003f4p4

Первые проблемы

Если открыть любой мануал по работе с платой, то выяснится, что для работы с ней нужен целый отладчик, а это уже дополнительные затраты и неудобное подключение. И казалось бы, даже в Arduino IDE добавили поддержку этого микроконтроллера, но вот даже там нужно использовать программатор. Ардуино великолепна своей простотой, ведь всё, что нужно, чтобы ее программировать, — сама платка, ну, может, еще и кабель.

При этом сам по себе камень ATmega328 не имеет возможности прошивать его с завода, в него на этапе производства или уже на этапе выпуска платы зашивается бутлодер, который и позволяет загружать во внутреннюю память контроллера любую программу без необходимости использования отладчика. Чаще всего используется как раз USB-UART от компании WCH, он дает USB-интерфейс плате с контроллером, которая изначально лишена поддержки USB.

Плата у меня на руках, и основной интерес вызывает USB-порт. Для чего же он?

Например, плата от WeAct позволяет использовать его как настоящее USB-device устройство, хотя сам МК не поддерживает протокол, умельцы научились эмулировать USB-накопитель при помощи GPIO, обычных пинов или в простонародье — ногодрыгом. Это и было моей основной теорией.

Мы все заметили, на плате нет LDO на 3,3 В, есть только защитный диод по входу питания с Type-C, а для работы USB необходимо напряжение 3,3 В, получается не оно. Пока я шел домой с покупкой, думал, что, может, все-таки удастся завести USB на 5 В, ну, может, на 4,5 В за счет диода, ну или, может, в даташите описано, что внутри платы есть встроенная понижайка, ведь по картинке видно, что резисторов маловато, чтобы на них сделать два делителя.

Оказалось, что на выводы DP и DM у этого USB выведен UART. Просто пины PA5 и PA6 без какой-либо обвязки. Идея до сих пор кажется мне странной, но определенная красота в ней есть.

Для работы с этой платой нам в любом случае недостаточно обычного кабеля, но можно один раз собрать себе такой кабель, думаю, любители DIY справятся, ну или на крайний случай заказать уже готовый (с UART-ом в USB-C), чтобы прошивать им платы без возни с разъемами.

УК РФ не обязывает волка соблюдать логические уровни TTL, всё и так работает, а потому встает вопрос, какой же будем использовать загрузчик АУФ.

Загрузчик (bootloader)

Официальный репозиторий openwch примеров к контроллеру показывает пример загрузчика и софта для работы с ним. Это уже лучше, чем ничего.

загрузчик от openwch

загрузчик от openwch

Собираем, пробуем, загружаем и сразу находим минусы:

  • требуется постоянно переключаться между окнами разработки

  • нужно повторно подключать кабель и удерживать кнопку, чтобы попасть в бут

  • работает только под Windows

К моей удаче, нашелся добрый человек со знанием GO, и именно на этом языке он написал свой загрузчик, поддерживающий протокол из примера WCH, только при помощи командной строки. Он решает две проблемы из трех, что уже здорово, остальное не лень сделать и самому.

А раз уж делаем, перечислим наши пожелания ко всему проекту:

  1. Удобная среда разработки.

  2. Возможность компиляции и загрузки кода одним нажатием кнопки.

  3. Управляемый светодиод прямо на плате.

  4. Небольшой объем памяти нужен под настройки, что-то вроде EEPROM.

  5. Удобный терминал, как в Arduino IDE.

  6. Удобный plotter, как в Arduino IDE.

  7. Возможность отладки через дебаггер.

Ну и дальше по плану:

1 — Visual Studio Code

Здесь всё просто: мы выбираем самое гибкое и современное решение. К тому же, оболочку с cmake для наших микроконтроллеров уже собрали до нас, чем и пользуемся.

Однако у меня возникли проблемы с GCC от xPack: он некорректно отображает объём используемой памяти. Это некритично, но всё же неприятно. Потому я решил вытащить GCC-12 из MounRiver Studio, он работает, но чутка не open source. Постараюсь держать руку на пульсе, если удастся исправить и эту проблему, то обновлю комплект в репозитории на открытый GCC.

Настройку cmake файлов я счёл неинтересной, а единственные сложности, которые встретил, — нужно было выкинуть из startup_ch32v00x.S лишний мусор, который съедал память, а также подправить флаги сборки также для минимизации используемой flash.

2 — Task Runner

Упомянутая IDE предоставляет возможность устанавливать расширения, и в дальнейшем мы будем активно их использовать. Первым рассмотрим расширение Task Runner, которое позволяет запускать скрипты через GUI кнопочки, настраиваемые в файле tasks.json, для начала можно настроить сборку:

{     "label": "[build]...............cmake build",     "type": "shell",     "command": "cmake --build ${command:cmake.buildDirectory} --target all",     "problemMatcher": "$gcc" }

Просто вставлю красивую картинку

красивая картинка

красивая картинка

2.1.1 — Изменение бутлодера

В текущей реализации для перепрошивки необходимо извлечь провод, нажать кнопку и вставить его обратно. Это довольно сложная процедура. Для начала попробуем упростить её.

Нам доступен только UART, поэтому нужно научиться принимать команды через него. Вариант приема сообщением в блокирующем режиме отметаем как некрутой. Остается два варианта: использовать прерывания при получении каждого байта или настроить DMA.

Первый способ предполагает использование второго потока. Следует помнить, что при длительном отключении прерываний мы можем потерять часть или всю команду. Тем не менее, этот способ рабочий, проблемы если и будут, то у людей, которые знают как их решать. Кроме того, он позволяет использовать немного меньше памяти, учитывая ограниченный объём программного пространства в 16 килобайт.

Второй вариант позволяет реализовать всю бизнес-логику обработки в main-процессе, а DMA был создан именно для этого.

Логика на базе Arduino стремится к максимальной прозрачности и простоте в работе. Именно поэтому я выбрал второй вариант.

2.1.2 — Настройка DMA

Настраиваем циклический буфер на 5 канале DMA:

DMA каналы (скрин из RM)

DMA каналы (скрин из RM)

Создаём переменную для хранения принятых байт и настраиваем регистры DMA, используя аналог HAL от WCH:

static u8 RxDmaBuffer[100] = {0};  void _usart_dma_init(void) {     DMA_InitTypeDef DMA_InitStructure = {0};     RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);      DMA_DeInit(DMA1_Channel5);     DMA_StructInit(&DMA_InitStructure);     DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)(&USART1->DATAR);     DMA_InitStructure.DMA_MemoryBaseAddr = (u32)RxDmaBuffer;          DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;     DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;     DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;     DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;     DMA_InitStructure.DMA_BufferSize = sizeof(RxDmaBuffer);      DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;     DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;      DMA_Init(DMA1_Channel5, &DMA_InitStructure);     DMA_Cmd(DMA1_Channel5, ENABLE); /* USART1 Rx */ } 

2.1.3 — Настройка таймера

При пустом loop() проверка происходит либо слишком часто, либо слишком медленно.Предполагаемый пользователь фреймворка должен иметь возможность использовать его без лишних сложностей. Дополнительно добавим таймер, который позволит отслеживать, сколько времени прошло с момента последнего изменения счетчика принятых байтов.

void _usart_tim_init(void) {     TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure={0};      RCC_APB2PeriphClockCmd( RCC_APB2Periph_TIM1, ENABLE );      TIM_TimeBaseInitStructure.TIM_Period = 60000;     TIM_TimeBaseInitStructure.TIM_Prescaler = 48000-1;     TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;     TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;     TIM_TimeBaseInit( TIM1, &TIM_TimeBaseInitStructure);      TIM_CtrlPWMOutputs(TIM1, ENABLE );     TIM_ARRPreloadConfig( TIM1, ENABLE );          TIM_Cmd( TIM1, ENABLE );    }

При такой настройке счётчик таймера будет увеличиваться каждую миллисекунду до тех пор, пока не достигнет 60 секунд. Для базового функционала приёма целых команд этого более чем достаточно.

Для фиксации команды достаточно проверить, не менялся ли счётчик байтов DMA, например, в течение 5 миллисекунд. Напомню, что мы разрабатываем простой интерфейс для отправки элементарных команд.

Код может показаться не очень красивым и интересным, но я рекомендую не обращать на это внимания и двигаться дальше. Если у вас есть идеи, как сделать его более привлекательным, вы всегда можете оставить комментарий или отправить запрос на GitHub.

static void _uart_commands_loop() {     static uint8_t last_dma_count = 0;     static uint8_t last_dma_check = 0;     const uint8_t timer_check_count = 5;      uint8_t current_dma_count = _get_dma_count();     if (current_dma_count != last_dma_count) {         _uart_timer_start();     }     last_dma_count = current_dma_count;      if (TIM_GetCounter(TIM1) > timer_check_count) {         _uart_timer_stop();         char command[sizeof(RxDmaBuffer) / 2] = {0};         if (_get_dma_string(last_dma_check, current_dma_count, sizeof(command) - 1, command) > 0) {                printf("try_run_uart_command [%s]\r\n", command);             _try_run_uart_command(command);         }         last_dma_check = current_dma_count;      } }

Для перевода микроконтроллера в режим бутлодера нужно отправить в консоль строку: «command: reboot bootloader«. 

Недостатков у получившегося метода более, чем достаточно, я перечислю их за вас:

  • Если долго висеть в loop(), то мы не увидим ответа.

  • Если долго висеть в loop() в консоль могут попасть данные, не относящиеся к команде.

  • Минимальная задержка составляет 5 миллисекунд + время выполнения одного loop() после полной отправки команды.

Преимущества очевидны и перевешивают:

  • Простота.

  • Предсказуемость.

  • Проверенная эффективность.

2.1.4 Команды

Идея с командами мне показалась настолько интересной, что я решил, что неплохо было бы добавить их в юзерспейс. Если команда отличается от системной, то можно ее перехватить в методе command_callback.

void command_callback(const char* cmd) { const char prefix[] = "mode "; if (strlen(cmd) < sizeof(prefix)) { printf("argument error"); return; } if (strncmp(cmd, prefix, sizeof(prefix) - 1) == 0) { switch (cmd[sizeof(prefix) - 1]) { case '0': config.mode = 0; break; case '1': config.mode = 1; break; default: printf("argument error"); return; } save_config(&config.raw); printf("mode changed to %d\r\n", config.mode); } }

Обратите внимание на save_config и config.mode, вероятно вы уже поняли к чему они тут.

Чтобы попасть в метод, нужно отправить строку «command: mode 1» в консоль с чипом. Я добавил пример в TaskRunner для mode 0 и 1, вам же предлагается поменять и коллбек и команды по собственному усмотрению.

Пример из файла tasks.json:

{     "label": "⚙️ Example cmd (mode 0)",     "type": "shell",     "command": "python",     "args": [         "..\\tools\\serialsend.py",         "COM18",         "460800",         "command: mode 0"     ] }

3.0 Светодиод

Что-то мигает

Что-то мигает

На плате имеется два светодиода: один из них служит индикатором питания, а второй может быть использован по вашему усмотрению. Светодиод подключен к порту PD1, который, в свою очередь, служит для программирования чипа через дебаггер.

Прежде чем приступить к программированию, необходимо отключить режим прошивки. Для этого следует изменить конфигурацию порта и настроить альтернативные функции.

Port Configuration Register Low (GPIOx_CFGLR)

Port Configuration Register Low (GPIOx_CFGLR)
Remap Register 1 (AFIO_PCFR1)

Remap Register 1 (AFIO_PCFR1)

И теперь в виде кода:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE); GPIO_InitTypeDef GPIO_InitStructure = {0}; GPIOD->CFGLR &= ~( 0b11 << 6 ); u32 tmp = AFIO->PCFR1 & (~(0b111 << 24)); tmp |= 0b100 << 24;AFIO->PCFR1 |= tmp; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOD, &GPIO_InitStructure);

Светодиод завелся и работает, а результат есть на гифке из начала статьи.

3.1 Светодиод — минусы

Признаться, ровно с этого и началась моя работа над подобным подходом к чипу, то есть ардуино-формат. Я хотел дешевую плату со светодиодом, да и отладчик у меня есть, но так вышло, что я могу использовать либо одно, либо другое.

Пользоваться дебаггером, бегать по регистрам, выставлять брейкпоинты всё так же можно, и даже не выходя из VS Code, теряется лишь работа с портом как с GPIO. А для удобства все спорные моменты вынесены в корневой CMakeLists.txt.

CMakeLists.txt

CMakeLists.txt

Так, для подключения дебаггера нужно будет убрать дефайн USE_PROGRAMMING_PIN_AS_GPIO, закомментировав строку 11.

Если я хочу вернуться к MRS и дебаггеру мне нужно перешивать твою плату?

К счастью, не нужно. В режиме бутлодера пин PD1 не задействован, поэтому вы можете свободно подключать плату через WCH-LinkE. Чтобы перейти в этот режим, достаточно нажать единственную кнопку на плате, даже во время её работы.

4. EEPROM

Не совсем EEPROM, и даже не совсем флеш, но всё же. У ATMEGA из Arduino есть специальная область памяти, которая прекрасно подходит для хранения небольших данных.

Мне нужно примерно 10 байт для хранения таких настроек, как режим работы, частота передачи и локальные параметры. Плюс ещё около 30 байт для хранения имени пользователя и пароля.

Заявленная ёмкость памяти чипа составляет 10 000 циклов, что должно быть достаточно для изменения настроек только пользователем.

Размер страницы в ch32v003 — 64 байта (стереть меньше этого объема нельзя), а значит на все про все хватит и одной странички, и еще останется место на CRC.

Мы ограничиваем размер прошивки двумя страницами. Вторая страница нужна на случай, если во время записи в память произойдёт обрыв питания. В таком случае мы, возможно, не запишем новые настройки, но когда питание будет восстановлено, в памяти останутся хотя бы старые параметры.

Link.ld

Link.ld

И делаем обвязку вокруг этих страниц флеш в виде понятных методов, которые принимают на вход ссылку на config_t, который под капотом просто массив на 62 байта (два под флаг и crc):

read_config(&config.raw); save_config(&config.raw);

Вот пример, который поможет понять, как это работает. Мы создаём структуру с необходимыми параметрами и объединяем её с config_t через оператор union. Это позволяет объекту raw находиться в тех же местах оперативной памяти, что и переменные, что делает его использование очень интуитивным.

union config_u {     config_t raw;     struct {         uint8_t mode;         uint8_t mac[6];         uint8_t ipv4[4];         char password[32];     }; } config;  ...   read_config(&config.raw); ...  ...     if (config.mode == 0) {     delay(100);   } else if (config.mode == 1) {     delay(500);   } ...

Также пришлось поработать над бутлодером, так как оригинальный вариант полностью удаляет флеш-память, а нам нужно было оставить часть. Этот кусок я решил оставить за кадром.

Если вам интересно, вы можете изучить код бутлодера, он находится в папке tools в виде архива. Я не стал детально разбирать его, так как сам код вышел довольно колхозный, но рабочий, и тратить время на его прилизывание я не стал. Тем не менее, оригинальный загрузчик в формате exe, взятый с GitHub-репозитория openwch, корректно работает с моим бутлодером.

5. Монитор порта

Я не стал заниматься созданием велосипедов, так как VS Code предоставляет возможность добавлять любой функционал с помощью расширений. Я изучил самые популярные из них и выбрал то, что показалось мне наиболее удобным. Кстати, в этом расширении есть функция сохранения консоли в файл.

так выглядит терминал

так выглядит терминал

6. Плоттер

Тут, на удивление, аналогично. У получившегося решения гораздо больше возможностей, чем у плоттера Arduino. Оно отображает текущее значение и позволяет просматривать логи, исключая из них ненужные данные. Есть возможность разделить графики и настроить масштаб по своему усмотрению.

Для вывода значения необходимо начать строку с символа «>», передать название переменной и через двоеточие значение. В одной строке можно передавать несколько значений, разделяя их запятой.

В примере ниже я подключил потенциометр к пину D4, вывел значение с него, с Vref (на котором постоянное напряжение 1,2 В) и простейший счетчик, который увеличивается от 300 до 1000 и сбрасывается.

printf(">D4_voltage:%d\r\n", analogRead(D4)); printf(">Vref_voltage:%d\r\n", analogRead(Aref)); printf(">counter:%d\r\n", counter);
так выглядит плоттер

так выглядит плоттер

7. Отладка с дебаггером

Как уже упоминалось в пункте 3.1, идем в CMakeLists.txt и выставляем там дебаг (обратите внимание на гиф). ВАЖНО!!! для отладки нужен специальный программатор wch-linkE, обычного usb-uart недостаточно.

так выглядит дебаг

так выглядит дебаг

Я не буду углубляться в детали отладки, но вы можете убедиться, что OpenOCD работает, посмотрев значение переменной counter

Итог пессимиста

Вынужден заметить, что есть отличия от Arduino IDE, которые я так и не смог убрать:

  • Бутлодер нужно загружать вручную (или покупать плату с уже установленным бутлодером).

  • Кабель также необходимо изготавливать самостоятельно (или приобретать уже готовый).

  • Для перепрошивки требуется отключать монитор и плоттер.

  • Использование библиотеки printf и string занимает значительную часть памяти.

Последний пункт можно отключить с помощью CMake, и тогда минимальная прошивка с blink-ом будет занимать менее 2 кБайт. Все результаты далее для конфига типа Debug.

Минимальная прошивка

Минимальная прошивка

Но для ее использования придется передергивать питание и зажимать бут каждый раз при прошивке.

Можно немного изменить ситуацию и разрешить использование кнопки на плате и во время прошивки. Это может занять ещё один килобайт памяти, но зато пользоваться функцией станет удобнее. Однако это всё равно неудобно.

Кнопка бута во время работы программы

Кнопка бута во время работы программы

Ну и минимальный приятный конфиг, с которым не нужно вставать с дивана. Он занимает всего 4 килобайта, что составляет четверть доступной памяти. В этом конфиге не используется printf.

Прошивка без нажатия на кнопку

Прошивка без нажатия на кнопку

Режим сборки Release ужимает последний до 3700, потому отдельно его не привожу, чудес не будет.

Итог оптимиста

Работа с платой стала настолько простой, что мне захотелось использовать её во всех своих проектах. Или, скорее, придумывать проекты, которые можно было бы реализовать на этой плате.

Я всегда мечтал иметь возможность измерять потребление тока в диапазоне от микроампер до единиц миллиампер. Для этого я заменил шунт на модуле с INA219, расширив диапазон измерений до 1 мкА — 30 мА. На мой взгляд, это идеальный вариант для измерения потребления платы с CH582M, но вот с ESP32 такой подход уже не сработает. На гиф ниже, измеряется потребление резистора 300к.

У Алекса Гувера вышел видос про его новые умные часы и мне захотелось поиграться с лентой на ws2812 с координатами hex.

Удовольствие получено, плата оправдала все свои запросы, а проект отправляется покорять OpenSource.


ссылка на оригинал статьи https://habr.com/ru/articles/852908/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *