
❯ Вступление
Давно хотел сделать устройство с USB, но больше всего мне казалось интересным это использовать программное USB. И для микроконтроллера CH32V003 давно существует библиотека rv003usb, которая решает эти задачи. В этой статье можно было бы рассказать как делать простейшее USB-устройство на основе её, но эта библиотека заметно больше и предлагает, помимо самой библиотеки, несколько полноценных рабочих программ: загрузчик и программатор.
Изначально я думал написать статью после создания готового устройства, но пока им занимался столкнулся с тем, что информации получается много и она интересная, поэтому решил разбить это все на части. Это первая, ознакомительная. Я как всегда начал разработку с прототипов и сделал тестовые платы с USB, чтобы “прощупать почву” и про них сегодня будет разговор. Но ради интереса еще попробуем загрузчик и соберем программатор из этой же библиотеки.
Попробуем сделать свой Digispark!
❯ Введение
До того, как я еще не знал про CH32V003, я присматривался к ATTiny13 и ATTiny85. Для ATtiny13 и речи не может быть про работу с USB, а вот для ATtiny85 есть отдельные отладочные платы под названием Digispark, в которой как раз используется программный USB. Про неё, возможно, я узнал из YouTube-роликов от AlexGyver о его библиотеке для создания клавиатуры или мыши через программный USB, EasyHID. Так в этой библиотеке была замечена очень интересная схема:

Как позже я узнал, что эта схема подключения изначально появилась из библиотеки V-USB, на которой основывалась EasyHID:

Время шло и я уже узнал про CH32V003, что он стоит значительно меньше, чем ATTiny85, а по функционалу даже её превосходит. При этом у меня появился фрезерный станок и я стал больше экспериментировать с самодельными платами. Поэтому было решено делать USB устройство на CH32V003, т.к. для него уже есть библиотека для работы с программным USB, rv003usb.
И если продолжать рассуждения, то в rv003usb
лежит уже вот такая схема:

Ответ скрывается в том, что для работы с USB нужны 4-е линии: питание, земля, D+, D-. Питание от USB это 5V, а вот D+ и D- не должны превышать 3.6V. Поэтому в каждой схеме подключения USB к микроконтроллеру используется свой подход, но идеи в основном две: питаем микроконтроллер от 5V, тогда нужно D- и D+ ограничить (через стабилитроны) или питаем микроконтроллер от 3.3V, тогда ничего ограничивать не нужно (через стабилизатор напряжения или 2 последовательных диода по питанию).
В rv003usb
используется подход со стабилизатором напряжения и поэтому стал его в дальнейшем использовать, но скорее всего все должно работать и если использовать другие схемы, но я их еще не проверял.
❯ Отладочная плата
Первые проблемы
В итоге я сделал три отладочные платы с usb, кнопкой и светодиодом, и одну с тем же набором, но с SPI флеш-памятью. Еще 2 платы сделал на станке, но уже не расставлял компоненты, т.к. они уже не понадобились. Итого 6. Почему так много, если по-хорошему достаточно только 2: одну простую и одну с SPI флеш-памятью?
Все просто, сначала я спаял первую плату и когда зашил тестовый пример (demo_gamepad), то он у меня не завелся. (В репозитории к этой статье вы можете найти все сопутствующие материалы как, например, принципиальные схемы.) Я перепробовал все настройки, но успеха не достиг, поэтому решено было делать вторую плату, где добавил подключение DPU и резисторы на линии D-, D+, но тоже успеха не достиг. Сделал третью плату в которую для последующий экспериментов добавил SPI Flash память, но она тоже не завелась, надежды были, что там используется CH32V003F4P6, а не CH32V003J4M6, но оказалось не в этом проблема.
Мне казалось, что я все перепробовал, поэтому решено было на последние меры — заменить разъем USB.
Я начал с замены Type-C на Micro, параллельно делая еще две платы с другими USB разъемами Просто я наверняка знал, что эта схема рабочая, т.к. с Micro USB видел плату у wagiminator. Сначала она тоже не заработала, но потом я скомпилировал пример в Linux, залил его в микроконтроллер, отсоединил кабель из USB изолятора и вставил его в другой разъем компьютера. И тут вы могли спросить меня «Что это за беспредел?» и оказались бы правы. Я наслышался в сети, что если что-то сделаю не правильно, то могу спалить USB порт компьютера (или материнскую плату) из-за короткого замыкания, поэтому приобрел USB изолятор. В общем, когда я зашил программу, скомпилированную в Linux, вытащил кабель из изолятора и вставил его в другой разъем, то плата определилась.
В итоге причины было 2. Первая, rv003usb
не работает c USB изолятором. И вторая, что я использовал не Makefile-ы, а PlatformIO, а он оказался не совместим с rv003usb
, точнее там была ошибка, но почему-то никто до этого раньше её не нашел. Всему причина был флаг -DCH32V00x
, я его убрал и проект в PlatformIO тоже заработал. (О проблеме я написал на странице проекта и пока писал эту статью мой пулл-реквест одобрили и теперь проблема исправлена.) В любом случае для работы с rv003usb
важно уметь собирать проект из командной строки, как это увидим дальше при работе с загрузчиком или программатором.
И вот поэтому плат оказалось 6 штук, а две из них остались не собранными, не пригодились. Другими словами, очень сильно хотел, чтобы у меня заработал программный USB и перестарался.
Схемы отладочной платы

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

В статье я не планирую долго объяснять значение каждого параметра, для этого есть страницы проектов, к которым лучше обращаться в любом случае, но основные знания для понимания контекста постараюсь дать. Поэтому рассмотрим сигналы D+, D-, DPU, а точнее их подключение. Как можете видеть в схеме подключения программного USB из rv003usb
, что от USB идут два провода данных D+, D-, а вот DPU объединяется с D- и нужен он для того, чтобы сообщить компьютеру, к кому мы подключаем устройство, о своем присутствии. Т.е. например подключили устройство к компьютеру, поработали минуты и отключились через сигнал DPU. Вот так все просто. Но можно жить и без DPU, если это не нужно, для этого надо D- подтянуть к питанию, как я сделал в первой плате. Но есть нюанс, возможно нам надо будет сообщить компьютеру о том, что он переназначил нас (re-enumerated), т.е. послать ему сигнал об отключении и подключении, так вот если мы собираемся так делать, то надо последовательно в линии D+, D- поставить резисторы 33 или 47 Ом. А вот их я и забыл поставить, точнее подумал, что и без них справлюсь. (Но отмечу тут, что я поставил резисторы 100 Ом в другой плате и у меня переназначение не получалось сделать, поэтому запаял на их место 47 Ом и все заработало, поэтому в схеме Гайвера могут быть соответствующие проблемы.)
Точнее я думал, что мне переназначение не понадобиться, но когда поставил USB-загрузчик, про который расскажу дальше, то оказалось, что очень даже нужно. Потому что логика работы с загрузчиком следующая: запускается загрузчик, определяется в системе как USB устройство и передает управление основной программе. Если не подать правильную последовательность сигналов в самом начале запуска основной программы (не загрузчике), а в первой плате у меня не получилось её никак подать из-за неправильной разводки, то компьютер не поймет, что мы теперь должны считаться как новое устройство, он будет продолжать думать, что мы USB загрузчик.
Если был бы сигнал DPU, то этих проблем можно было избежать. У кого-то может возникнуть вопрос, а как без сигнала DPU сделать переназначение? Ответ: надо перед началом функции usb_setup
подать низкий уровень на линии D-, пример этого, есть в коде программы к этой статье.
Поэтому принимайте улучшенную версию:


На примере этой платы рассмотрим основные настройки, которые стоит добавить в файл usb_config.h
:
#define USB_PORT C // [A,C,D] GPIO Port to use with D+, D- and DPU
#define USB_PIN_DP 2 // [0-4] GPIO Number for USB D+ Pin
#define USB_PIN_DM 4 // [0-4] GPIO Number for USB D- Pin
#define USB_PIN_DPU 1 // [0-7] GPIO for feeding the 1.5k Pull-Up on USB D- Pin; Comment out if not used / tied to 3V3!
В принципе достаточно сделать эти изменения в usb_config.h
и можно тестировать пример из библиотеки, например, demo_gamepad, и он должен запуститься.
❯ Работаем с библиотекой
Т.к. я планирую следующим проектом делать USB клавиатуру, то в отладочной плате добавил кнопку и светодиод, по-минимуму. (А в другой добавил SPI флеш-память, но об этом поговорим в другой раз.) Давайте сделаем следующие задачи: отправим сигналы с компьютера на отладочную плату, чтобы поморгать светодиодом; отправим сигнал о нажатие кнопки с отладочной платы на компьютер (как простая кнопка и как кнопка громкости); сделаем, что состояние о статусе Caps Lock с компьютера отображалось на светодиоде отладочной платы. Думаю этих задач нам хватит для того, что познакомится с библиотекой.
Пример 1. Отправляем сигнал с компьютера на устройство
В сети есть хорошие уроки от Code and Life, к которым я обращался и которые адаптировал под себя и под эту статью.
Для того, чтобы начать что-то принимать на устройстве по USB нужно добавить поддержку обработчиков, для этого нужно изменить флаги в usb_config.h
. Мы будем принимать сообщения управления, поэтому изменяем следующий код:
#define RV003USB_OTHER_CONTROL 1
Дальше нужно добавить функцию usb_handle_other_control_message
в main.c
, иначе будет ошибка при компиляции:
void usb_handle_other_control_message(struct usb_endpoint * e, struct usb_urb * s, struct rv003usb_internal * ist) {
if (s->wRequestTypeLSBRequestMSB & USB_TYPE_VENDOR) {
uint8_t bRequest = s->wRequestTypeLSBRequestMSB >> 8;
if (bRequest == 1) {
funDigitalWrite(LED, FUN_HIGH);
} else if (bRequest == 0) {
funDigitalWrite(LED, FUN_LOW);
}
}
}
В функции хорошо видно, что мы моргаем светодиодом, но многое все равно не понятно. Но для понимания достаточно знать, что в s->wRequestTypeLSBRequestMSB
приходит информация о запросе с флагами. Флаг USB_TYPE_VENDOR
означает, что это созданный нами запрос, потому что в спецификации USB уже зарезервированы номера запросов и 0 и 1, что-то значат, а т.к. мы указали, что этот запрос от производителя, то они будут значит то, что мы решим.
Остальной код устройства опущу, он хоть и важный, но для понимания не критичный. Посмотреть его можете в репозитории.
Теперь сделаем небольшую консольную программу на C#, которая отправляет эти сигналы по USB. (Можно и на C++, но мне C# ближе и объяснять будет легче, т.к. читается проще.)
Если вы пользуетесь Windows, то для работы нам понадобится библиотека libusb-win32
, а точнее надо будет скачать последний архив с сайта и запустить программу install-filter-win.exe
. В нем найти наше устройство и нажать install. Оно будет обозначаться так:

После чего библиотека libusb-win32
, которую мы планируем использовать, может отправлять сигналы по USB.
Вы можете задаться вопросом откуда взялся идентификатор USB 0039:0001. Эти данные находятся в usb_config.h
, мимо них вы не пройдете. Там много что еще осталось, по ходу дела постараюсь все объяснить, что вспомню.
Программу по отправке сообщений приведу полностью, т.к. она получилась небольшая:
using LibUsbDotNet;
using LibUsbDotNet.Main;
using System.Runtime.InteropServices;
public class DevboardControlProgram
{
public static void Main(string[] args)
{
if (args == null || args.Length <= 0)
{
Console.WriteLine("Usage:\n DevboardControl.exe on\n DevboardControl.exe off");
return;
}
var request = (byte) (args[0].ToLower() == "on" ? 1 : 0);
UsbDevice devboard;
int vendorId = 0x0039;
int productId = 0x0001;
devboard = UsbDevice.OpenUsbDevice(new UsbDeviceFinder(vendorId, productId));
if (devboard == null)
{
Console.WriteLine("Device Not Found.");
return;
}
try
{
byte[] buffer = new byte[0];
int bytesRead;
var packet = new UsbSetupPacket(
(byte)UsbEndpointDirection.EndpointIn | (byte)UsbRequestType.TypeStandard | (byte)UsbRequestRecipient.RecipDevice | (byte)UsbRequestType.TypeVendor,
request,
0x0,
0x0,
0x0
);
var result = devboard.ControlTransfer(ref packet, buffer, buffer.Length, out bytesRead);
if (!result)
{
Console.WriteLine("Error during USB interaction");
}
}
finally
{
if (devboard != null)
{
devboard.Close();
}
}
}
}
Как можно видеть она очень простая: получаем аргументы из командой строки, находим USB-устройство по идентификатору 0039:0001, формируем пакет и отправляем его. Здесь важно, что мы добавили TypeVendor
и в данных для пакета указали запрос (request). Остальные данные или не заполняются или используются по-минимуму.
Теперь если выполнить команду с компьютера DebugControl.exe on
светодиод зажжется, а DebugControl.exe off
погаснет.
❯ Пример 2. Имитируем нажатие клавиатуры
Давайте теперь сделаем, что при нажатии по кнопке на отладочной плате на компьютере что-то печаталось, например буква А. Или включалась, выключалась громкость. Но можно сделать и то и другое.
Изменяем файл usb_config.h
:
Скрытый текст
static const uint8_t keyboard_hid_desc[] = { /* USB report descriptor */
HID_USAGE_PAGE( HID_USAGE_PAGE_DESKTOP ),
HID_USAGE( HID_USAGE_DESKTOP_KEYBOARD ),
HID_COLLECTION ( HID_COLLECTION_APPLICATION ),
// Modification key
HID_USAGE_PAGE( HID_USAGE_PAGE_KEYBOARD ),
HID_USAGE_MIN( HID_KEY_CONTROL_LEFT ),
HID_USAGE_MAX( HID_KEY_GUI_RIGHT ),
HID_LOGICAL_MIN( 0 ),
HID_LOGICAL_MAX( 1 ),
HID_REPORT_SIZE( 1 ),
HID_REPORT_COUNT( 8 ),
HID_INPUT( HID_DATA | HID_VARIABLE | HID_ABSOLUTE ),
// 1 byte reserved
HID_REPORT_SIZE( 8 ),
HID_REPORT_COUNT( 1 ),
HID_INPUT( HID_CONSTANT | HID_VARIABLE | HID_ABSOLUTE),
// Leds, 5 bits
HID_USAGE_PAGE( HID_USAGE_PAGE_LED ),
HID_REPORT_SIZE( 1 ),
HID_REPORT_COUNT( 5 ),
HID_USAGE_MIN( 0x01 ), // Num Lock
HID_USAGE_MAX( 0x05 ), // Kana
HID_LOGICAL_MIN( 0 ),
HID_LOGICAL_MAX( 1 ),
HID_OUTPUT( HID_DATA | HID_VARIABLE | HID_ABSOLUTE ),
// Padding, 3 bits
HID_REPORT_SIZE( 3 ),
HID_REPORT_COUNT( 1 ),
HID_OUTPUT( HID_CONSTANT | HID_VARIABLE | HID_ABSOLUTE ),
// Padding to fill the rest of the buffer to 8 bytes.
HID_REPORT_SIZE( 8 ),
HID_REPORT_COUNT( 7 ),
HID_OUTPUT( HID_CONSTANT | HID_VARIABLE | HID_ABSOLUTE ),
// 6 Keys
HID_USAGE_PAGE( HID_USAGE_PAGE_KEYBOARD ),
HID_REPORT_SIZE( 8 ),
HID_REPORT_COUNT( 6 ),
HID_LOGICAL_MIN( 0 ),
HID_LOGICAL_MAX( 0xff ),
HID_USAGE_MIN( 0 ),
HID_USAGE_MAX( 0xff ),
HID_INPUT( HID_DATA | HID_ARRAY | HID_ABSOLUTE ),
HID_COLLECTION_END,
};
static const uint8_t keyboard_media_hid_desc[] = { /* USB report descriptor */
HID_USAGE_PAGE( HID_USAGE_PAGE_CONSUMER ),
HID_USAGE( HID_USAGE_CONSUMER_CONTROL ),
HID_COLLECTION ( HID_COLLECTION_APPLICATION ),
// Mute key code
HID_USAGE ( HID_USAGE_CONSUMER_MUTE ),
HID_LOGICAL_MIN( 0 ),
HID_LOGICAL_MAX( 1 ),
HID_REPORT_SIZE( 1 ),
HID_REPORT_COUNT( 1 ),
HID_INPUT( HID_DATA | HID_VARIABLE | HID_ABSOLUTE ),
// Padding, 7 bits reserved
HID_REPORT_SIZE( 7 ),
HID_REPORT_COUNT( 1 ),
HID_INPUT( HID_CONSTANT | HID_VARIABLE | HID_ABSOLUTE ),
HID_COLLECTION_END,
};
Еще можно привести места где используются эти структуры, но для простоты изложения я это опущу, кому интересно могут заглянуть в код проекта. Здесь важно отметить, что для того, что клавиатура определилась как USB устройство, ей нужно задать HID дескриптор. HID дескриптор это стандартизированная структура и про неё много где есть информации, но я спрашивал чат ботов, они охотно проверяли и создавали её. Суть её в том, что она описывает в каком формате будут принимать данные USB-устройство или HID-устройство.
В keyboard_hid_desc
описано, что клавиатура будет принимать в сумме 8 байтов: 1-й байт - состояние клавиш модификаторов (Caps Lock, etc.), 2-й байт зарезервирован, следующие 6 байтов - состояние о 6 нажатых клавиш. Еще можно заметить в этой структуре описание состояния лампочек: 5 битов на лампочку, 3 бита зарезервировано, 7 байтов для наполнителя (он необходим, я даже сделал пулл-реквест, который исправляет эту ошибку в примерах rv003usb
, иначе ничего приходить не будет). Но о лампочка поговорим позже.
А keyboard_media_hid_desc
нужна, потому что у меня не получилось отправить состояние о нажатие громкости используя только keyboard_hid_desc
, пришлось указать, что на отладочной платы у нас 2 клавиатуры. Её формат данных очень простой: 1-й бит состояние о нажатой клавиши громкости, следующие 7 битов зарезервированы. Всего один байт.
Дальше вносим еще изменения в usb_config.h
:
#define RV003USB_HANDLE_IN_REQUEST 1
Теперь надо создать функцию-обработчик, которая будет вызываться библиотекой, когда наше устройство будет опрашиваться компьютером (как часто оно опрашивается указано в usb_config.h
, например, тут):
typedef struct TU_ATTR_PACKED
{
uint8_t modifier; /**< Keyboard modifier (KEYBOARD_MODIFIER_* masks). */
uint8_t reserved; /**< Reserved for OEM use, always set to 0. */
uint8_t keycode[6]; /**< Key codes of the currently pressed keys. */
} hid_keyboard_report_t;
hid_keyboard_report_t buttonData = {0};
void usb_handle_user_in_request( struct usb_endpoint * e, uint8_t * scratchpad, int endp, uint32_t sendtok, struct rv003usb_internal * ist )
{
if (endp == 2) {
uint8_t d;
if (buttonPressed) {
d = 1;
} else {
d = 0;
}
usb_send_data(&d, sizeof(d), 0, sendtok);
buttonPressed = 0;
}
else if( endp == 1 )
{
memset(&buttonData, 0, sizeof(buttonData));
if (buttonPressed) {
buttonData.keycode[0] = HID_KEY_A;
} else {
buttonData.keycode[0] = HID_KEY_NONE;
}
usb_send_data(&buttonData, sizeof(buttonData), 0, sendtok);
buttonPressed = 0;
}
else
{
usb_send_empty( sendtok );
}
}
Здесь уже можно сразу видеть, что мы отправляем состояние клавиши A, а если приглядеться, то в условии выше мы отправляем всего один байт, это и есть значение о состоянии клавиши громкости. Теперь при нажатии на клавишу на устройстве, будет печататься и буква А и включаться, выключаться громкость. Могут возникнуть вопросы, что такое endp
? Это endpoint, номер устройства (или интерфейса(?)), т.е. у первой клавиатуры у нас номер 1, а на второй номер 2. Узнать номера устройств можно в usb_config.h
. При чем usb_config.h
достаточно хорошо документирован.
Пример 3. Отображаем состояние Caps Lock на светодиоде
Мы уже научились моргать светодиодом с помощью консольной команды, но можно сделать заметно проще. В HID-дескрипторе мы уже указали, что наша клавиатура может отображать значение лампочек, таких как состояние Caps Lock. Но для того, чтобы получить информацию об этом, нужно сделать еще несколько шагов.
Добавляем в usb_config.h
:
#define RV003USB_HANDLE_USER_DATA 1
И теперь добавляем соответствующий обработчик в main.c
:
void usb_handle_user_data( struct usb_endpoint * e, int current_endpoint, uint8_t * data, int len, struct rv003usb_internal * ist )
{
if (current_endpoint == 0 && len > 0) {
if (data[0] & KEYBOARD_LED_CAPSLOCK) {
funDigitalWrite(LED, FUN_HIGH);
} else {
funDigitalWrite(LED, FUN_LOW);
}
}
}
В HID-дескрипторе указано, что первым битом идет Num Lock, за ним должен Caps Lock, этот код проверяет, если пришел нужный бит (второй), то включает или выключает светодиод. Так просто. (Дополнение: почему current_endpoint
получается 0, для меня пока загадка, возможно даже ошибка в библиотеке.)

❯ USB-загрузчик
Теперь самое полезное из этой библиотеки — загрузчик. Сначала я долго не мог понять как работает загрузчик, получается мы должны как-то разбивать свою программу на части, каждый раз в неё встраивать загрузчик. Cтроил различные теории. Но все оказалось проще, в даташите указано, что у CH32V003 есть загрузчик и под него выделено 1920 байтов. Это под основную программу только 16 КБ и они не пересекаются. Получается выполняется загрузчик, делает свое дело и передает управление основной программе.
Настроек у загрузчика из rv003usb
много, и их приводить не буду. Меня пока устраивает обычный режим: это 5 секунд загрузчик ждет команды от пользователя и если ничего не произошло, запускает основную программу. Есть, например, режим когда держишь нужную кнопку, включаешь плату в сеть и она входит в бесконечный режим загрузчика. Также можно изменять время пребывания в загрузчике, не только 5 секунд, но это ожидаемо.
Загрузчик находится в папке bootloader. Я сначала хотел его скомпилировать в PlatformIO, но ch32fun не позволяет указать другой файл линковки, а так как под загрузчик выделяется другой объем памяти это просто необходимо. Нашел для себя такое решение - собирать программу под Linux и перекидывать прошивку на основной компьютер. Наверно это все можно настроить и под Windows, но мне это не доставляло трудностей. И это достаточно сделать только один раз. (Хотя, скорее всего в следующий раз все настрою.)
Давайте для усложнения задачи добавим, что теперь наша программа по нажатию на клавишу входила в загрузчик. Вместо кнопки громкости и клавиши А.
Изменяем файл usb_config.h
загрузчика, добавляем уже знакомые нам константы:
#define USB_PORT C // [A,C,D] GPIO Port to use with D+, D- and DPU
#define USB_PIN_DP 2 // [0-4] GPIO Number for USB D+ Pin
#define USB_PIN_DM 4 // [0-4] GPIO Number for USB D- Pin
#define USB_PIN_DPU 1 // [0-7] GPIO for feeding the 1.5k Pull-Up on USB D- Pin; Comment out if not used / tied to 3V3!
И в bootlader.c
раскомментируем строчку:
#define SOFT_REBOOT_TO_BOOTLOADER
Компилируем загрузчик:
make clean && make
Копируем его на основной компьютер и зашиваем:
minichlink.exe -a -w bootloader.bin bootloader -B
Программа minichlink находится в проекте ch32fun
. Если вы пользуетесь PlatformIO под Windows, то она в C:\Users\<Username>\.platformio\packages\framework-ch32v003fun\minichlink
или в C:\Users\<Username>\.platformio\packages\tool-minichlink
. (Но вторая у меня оказалась проблемной, я создал issue и её уже заменили.) Зашивать загрузчик не через minichlink
я не решился, т.к. при беглом осмотре в WCH-LinkUtility не нашел кнопки залить в загрузчик, а побоялся ошибиться, указывая параметры (адреса, например) вручную.
Уже в проекте PlatformIO для отладочной платы нужно указать upload_protocol = minichlink
в platform.ini
. И теперь если отсоедините программатор от компьютера, подсоедините отладочную плату, и в течении 5 секунд попытаетесь зашить программу, то она зашьется без программатора. Очень удобно.
Осталось добавить в main.c
переход в загрузчик по нажатию на кнопку (этот код есть в комментариях в bootlader.c
):
if (buttonPressed) {
FLASH->BOOT_MODEKEYR = 0x45670123; // KEY1
FLASH->BOOT_MODEKEYR = 0xCDEF89AB; // KEY2
FLASH->STATR = 0x4000;
RCC->RSTSCKR |= 0x1000000;
PFIC->CFGR = 0xBEEF0080;
}
Теперь все. По нажатию на кнопку устройство переходит в режим загрузчика, также очень удобно.
Напомню, что если ошибетесь в разводке, забудете добавить 47 Ом резисторов по линии D-, D+ или решите убрать линию DPU, то плата так и останется видна в компьютер как загрузчик, она не сможет отвязаться и переназначить себя на новое устройство, чтобы например отправлять сигналы по нажатию на кнопку или отображать состояния кнопки Caps Lock. Но не всегда это проблема.
Важное дополнение: в ходе разработки у меня возник вопрос как вернуть загрузчик по-умолчанию, т.е. как избавится от USB-загрузчика, если он не понадобится больше. И как пишут в этом документе, что в открытом доступе нет загрузчика по-умолчанию, его исходный код закрыт. Получается, чтобы стереть загрузчик надо скомпилировать пустой загрузчик и зашить его. (Пример тестового загрузчика есть в ch32fun, да и про UART загрузчик была статья на Хабр.) Скорее всего это не критичная проблема, но упомянуть её стоит.
❯ Программатор
Еще интересный проект в библиотеке rv003usb
это собственный программатор, про который нигде ничего не написано. Не написано понятно почему, потому что всем достаточно WCH-LinkE, который и так не дорого не стоит. Но все же есть что-то притягательное сделать самому программатор, поэтому ради практики я решил развести схему и собрать его на основе проекта rvswdio_programmer.



Наблюдательный читатель может заметить, что в принципиальной схеме нет резисторов по D+, D- и нет DPU, тоже самое как в первой моей плате. Да, это я уже заметил позже, но это не ошибка потому что переназначение (re-enumeration) не будет, т.к. мы не используем в программаторе USB-загрузчик.
Еще я хотел сделать программатор на CH32V003J4M6, т.е. на микроконтроллере с 8-ю ножками, но не стал долго изучать этот вопрос, т.к. ножек очевидно не хватает. Хватало только на то, чтобы запрограммировать такой же CH32V003 микроконтроллер, т.е. на 3 вывода (SWIO, GND, VCC), а 4-й не умещался (SWCLK). Решил, что проще взять микроконтроллер с выводами побольше и сделать уже хорошо.
Для создания прошивки пришлось внести знакомые изменения в файл usb_config.h
:
#define USB_PORT A // [A,C,D] GPIO Port to use with D+, D- and DPU
#define USB_PIN_DP 2 // [0-4] GPIO Number for USB D+ Pin
#define USB_PIN_DM 1 // [0-4] GPIO Number for USB D- Pin
// #define USB_PIN_DPU 5 // [0-7] GPIO for feeding the 1.5k Pull-Up on USB D- Pin; Comment out if not used / tied to 3V3!
И rvswdio_programmer.c
:
#define PIN_SWCLK PC0
#define PIN_TARGETPOWER PD4
Далее любым (даже используя Arduino Nano) способом зашиваем прошивку в программатор и можно начать пользоваться. Для работы с ним необходимо использовать уже знакомую программу minichlink.exe
и для начала её можно запустить без аргументов, должно вывести что-то вроде этого:
PS C:\Users\m039> minichlink.exe
Dev Version: 4
Found ESP32S2-Style Programmer
Error: Setup chip failed. Got code 00000000
Could not setup interface.
Ошибка, из-за того, что он никуда не подсоединен. Вот, если подсоединить программатор к микроконтроллеру:
PS C:\Users\m039> minichlink.exe
Dev Version: 4
Found ESP32S2-Style Programmer
Interface Setup
Detected CH32V003
Flash Storage: 16 kB
Part UUID: d1-35-ab-cd-39-c9-bc-c7
Read protection: disabled
Для работы в PlatformIO нужно добавить строчку upload_protocol = minichlink
в platformio.ini
и можно пользоваться.
По первым впечатлениям все хорошо: микроконтроллеры видит и их прошивает. Я правда тестировал только CH32V003, потом попробую на CH32V203, но должно работать, как заявлено на странице проекта. Но дальше пошли очевидные проблемы.
Первая проблема — это отладка. При использовании WCH-LinkE создается виртуальный COM порт, к которому можно подключится и видеть отладочную информацию, если использовать, например, printf
в своем коде. Здесь же не создается никакого виртуального порта, но не все так плохо, считать отладочную информацию можно командой minichlink.exe -T
и оно будет работать. Неудобно только то, что надо её выполнять каждый раз, когда нужно отладить; виртуальный же порт можно постоянно держать открытым.
Вторая, и уже для многих критичная, проблема — это тоже отладка, а точнее отладчик. На странице проекта заявлено, что есть базовый функционал gdb, но у меня не получилось его запустить в PlatformIO, но правда он в нем и не должен работать. Но больше всего встревожило, что у меня не получилось отлаживать программу через консоль, т.е. поставить брейкпоинты, посмотреть регистры. Ничего. Возможно я что-то не так настроил, но маловероятно. Но поиском по страничке проекта в Github я нашел, что gdb разработчики не часто пользуются, поэтому скорее всего он не сильно отлажен.
Так, что если делать вывод, то собственный программатор не готов к полноценной работе из-за отсутствия отладчика, но как запасной вариант вполне. Я обычно пользуюсь printf
, а отладчиком редко, поэтому для базовых задач может хватить.
Дополнение: спросил про отладчик на странице в Github, пока ничего не ответили.
❯ Итог
По ходу статьи вы могли заметить, как я упоминал, что отправил то один запрос, то другой на страницу проекта rv003usb
и это важно, так как у вас должно сложится хорошее понимание о состоянии библиотеки. Что она легко может работать не так гладко, могут возникнут проблемы и некоторые моменты не протестированы, но в целом все очень здорово. Да и на любые запросы разработчики отвечают быстро. Здорово, что под такой простой микроконтроллер как CH32V003 есть такая библиотека с кучей работающих примеров. А попробовать USB-загрузчик однозначно стоит.
Если сомневаетесь, то можете посмотреть если ли пример для вашего проекта с этой библиотекой, если он есть, то скорее всего все проверено и оно работает, если нет, то готовьтесь собирать грабли. Если эта серия интересна, то напишу о моих граблях в похожем формате, в виде статьи или туториала.
Все схемы, исходные коды и фотографии плат находятся в репозитории rv003usb-playground. Этот репозиторий планирую использовать для дальнейших экспериментов, поэтому может что-то изменится, но постарался везде оставить пометки.
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩
Перед оплатой в разделе «Бонусы и промокоды» в панели управления активируйте промокод и получите кэшбэк на баланс.
VAF34
Проделана титаническая работа. Очень интересно1 Ранее в ряде форумов при обсуждении проблемы дополнительного USB для Arduino плат отвечали, что это очень сложно, почти не выполнимо.