Enjoying cheap Chinese microcontrollers (CH32V003)

If you find yourself here, then most likely you remember how back in 2022 one of the most important events in the world (DIY) was the news about microcontroller for 10 cents from Nanjing Qinheng Microelectronics Co., Ltd, further known as WCH, already known throughout the world thanks to its USB-UART whistle CH340.

I ordered debugging from WCH itself, a board from WeAct and even the stones themselves from Ali, tried a couple of examples and forgot. For DIY projects, I liked the boards from WeAct with ch32x035 and ch32v203 much more; they cost about the same and have much more functionality, but this year on the Chinese marketplace I started to come across a board with the hero of the article, and even with USB-C on her.

It costs noticeably less than its counterparts and at the time of ordering it cost me 90 rubles in total with delivery, which means there will be a new DIY king.

And so the idea was born to do your sdk.

cheap scarf with ch32v003f4p4

cheap scarf with ch32v003f4p4

First problems

If you open any manual for working with the board, you will find out that to work with it you need a whole debugger, which means additional costs and an inconvenient connection. And it would seem that even the Arduino IDE has added support for this microcontroller, but even there you need to use a programmer. Arduino is magnificent in its simplicity, because all you need to program it is the board itself, well, maybe also a cable.

At the same time, the ATmega328 stone itself does not have the ability to flash it from the factory; a bootloader is sewn into it at the production stage or already at the board release stage, which allows you to load any program into the internal memory of the controller without the need to use a debugger. Most often, USB-UART from WCH is used; it provides a USB interface to a board with a controller, which initially does not support USB.

I have the board in my hands, and the main interest is the USB port. What is it for?

For example, the board from WeAct allows you to use it as a real USB device, although the MK itself does not support the protocol; craftsmen have learned to emulate [USB-накопитель](https://github.com/cnlohr/rv003usb?ysclid=m1l2w9kbhn870348727) using GPIO, ordinary pins, or in common parlance – kicking. This was my main theory.

We all noticed that there is no 3.3 V LDO on the board, there is only a protective diode for the power input from Type-C, and for USB to work, a voltage of 3.3 V is required, which is not what it turns out. While I was walking home with the purchase, I thought that maybe it would still be possible to set up a USB at 5 V, well, maybe at 4.5 V using a diode, or maybe the datasheet describes that there is a built-in lower, because from the picture you can see that there are not enough resistors to make two dividers on them.

It turned out that this USB has a UART connected to the DP and DM pins. Just pins PA5 and PA6 without any wiring. The idea still seems strange to me, but there is a certain beauty in it.

To work with this board, in any case, a regular cable is not enough for us, but you can assemble such a cable for yourself once, I think DIY enthusiasts can handle it, or, as a last resort, order a ready-made one (with a UART in USB-C) to flash boards without fussing with connectors.

The Criminal Code of the Russian Federation does not oblige the wolf to comply with TTL logical levels, everything works that way, and therefore the question arises, what kind of AUF loader will we use?

Bootloader

Official repository openwch examples for the controller shows an example of a bootloader and software for working with it. This is already better than nothing.

bootloader from openwch

bootloader from openwch

We collect, try, download and immediately find the disadvantages:

  • you need to constantly switch between development windows

  • you need to reconnect the cable and hold the button to get into the boot

  • only works on Windows

Luckily for me, there was a kind person with knowledge of GO, and it was in this language that he wrote his loaderwhich supports the protocol from the WCH example, using the command line only. It solves two out of three problems, which is already great; the rest is not too lazy to do yourself.

And while we’re at it, let’s list our wishes for the entire project:

  1. Convenient development environment.

  2. Ability to compile and download code with the click of a button.

  3. Controllable LED directly on the board.

  4. A small amount of memory is needed for settings, something like EEPROM.

  5. Convenient terminal, like in Arduino IDE.

  6. Convenient plotter, like in Arduino IDE.

  7. Possibility of debugging via debugger.

Well, further according to plan:

1 – Visual Studio Code

Everything is simple here: we choose the most flexible and modern solution. In addition, the shell with cmake for our microcontrollers is already collected before us, which is what we use.

However, I'm having problems with xPack's GCC: it doesn't display memory usage correctly. This is not critical, but still unpleasant. That's why I decided to pull GCC-12 from MounRiver Studio, it works, but it's not open source. I’ll try to keep my finger on the pulse, if I can fix this problem, I’ll update the kit in the repository to open GCC.

I found setting up cmake files uninteresting, and the only difficulties I encountered were the need to remove excess garbage from startup_ch32v00x.S, which was eating up memory, and also adjust the build flags to minimize the flash used.

2 – Task Runner

The mentioned IDE provides the ability to install extensions, and in the future we will actively use them. First, let's look at the Task Runner extension, which allows you to run scripts through GUI buttons configured in the tasks.json file; first, you can configure the assembly:

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

I'll just insert a nice picture

beautiful picture

beautiful picture

2.1.1 – Changing the bootloader

In the current implementation, to flash the firmware, you need to remove the wire, press a button and insert it back. This is a rather complicated procedure. First, let's try to simplify it.

We only have access to UART, so we need to learn how to accept commands through it. We discard the option of receiving a message in blocking mode as uncool. There are two options left: use interrupts when each byte is received or configure DMA.

The first method involves using a second thread. It should be remembered that if interrupts are disabled for a long time, we may lose part or all of the command. However, this method works; if there are problems, then they will be with people who know how to solve them. In addition, it allows you to use slightly less memory, given the limited amount of program space of 16 kilobytes.

The second option allows you to implement all the business processing logic in the main process, and DMA was created precisely for this.

Arduino-based logic strives for maximum transparency and ease of operation. That's why I chose the second option.

2.1.2 – DMA setup

Setting up a circular buffer on DMA channel 5:

DMA channels (screen from RM)

DMA channels (screen from RM)

We create a variable to store received bytes and configure DMA registers using the HAL analogue from 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 – Setting the timer

With an empty loop(), the checking is either too frequent or too slow. The intended user of the framework should be able to use it without unnecessary complexity. Additionally, we will add a timer that will allow you to track how much time has passed since the last change in the counter of received bytes.

void _usart_tim_init(void){    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure={0};    RCC_APB2PeriphClockCmd( RCC_APB2Periph_TIM1, ENABLE );    TIM_TimeBaseInitStructure.TIM_Period = 60000;                        // ограничение времени таймера в 60 секунд    TIM_TimeBaseInitStructure.TIM_Prescaler = 48000-1;                         // частота HSI 48МГц, данная настройка дает                        // частоту таймера 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 );   }

With this setting, the timer counter will increment every millisecond until it reaches 60 seconds. For the basic functionality of receiving entire commands, this is more than enough.

To commit a command, it is enough to check whether the DMA byte counter has changed, for example, within 5 milliseconds. Let me remind you that we are developing a simple interface for sending basic commands.

The code may not seem very beautiful or interesting, but I recommend not paying attention to it and moving on. If you have ideas on how to make it more attractive, you can always leave a comment or submit a request on 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;     }}

To switch the microcontroller to bootloader mode, you need to send the following line to the console: “command: reboot bootloader“.

The resulting method has more than enough disadvantages, I will list them for you:

  • If we hang in loop() for a long time, we will not see the answer.

  • If you hang in loop() for a long time, data that is not related to the command may end up in the console.

  • The minimum delay is 5 milliseconds + the execution time of one loop() after the command is fully sent.

The advantages are obvious and outweigh:

2.1.4 Commands

The idea with teams seemed so interesting to me that I decided that it would be nice to add them to the userspace. If the command is different from the system one, then you can intercept it in the command_callback method.

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);	}}

Please note save_config And config.mode You probably already understand what they are talking about here.

To get into the method, you need to send the string “command: mode 1” into the console with the chip. I have added an example to TaskRunner for modes 1 and 2, but you are invited to change both the callback and commands at your own discretion.

Example from the tasks.json file:

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

3.0 LED

Something is blinking

Something is blinking

There are two LEDs on the board: one of them serves as a power indicator, and the second can be used at your discretion. The LED is connected to the PD1 port, which, in turn, serves to program the chip via a debugger.

Before you begin programming, you must disable the firmware mode. To do this, you must change the port configuration and configure alternative functions.

Port Configuration Register Low (GPIOx_CFGLR)

Port Configuration Register Low (GPIOx_CFGLR)

Remap Register 1 (AFIO_PCFR1)

Remap Register 1 (AFIO_PCFR1)

And now in code form:

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);

The LED started up and works, and the result is on the GIF from the beginning of the article.

3.1 LED – cons

Frankly, this is exactly where my work began on a similar approach to the chip, that is, the Arduino format. I wanted a cheap board with an LED, and I have a debugger, but it turns out that I can use one or the other.

You can still use the debugger, run through registers, set breakpoints, and even without leaving VS Code, you only lose working with the port as a GPIO. And for convenience, all controversial issues are listed in the root CMakeLists.txt.

CMakeLists.txt

CMakeLists.txt

So, to connect the debugger, you will need to undefine USE_PROGRAMMING_PIN_AS_GPIO by commenting out line 11.

If I want to go back to MRS and the debugger, do I need to change your board?

Fortunately, it's not necessary. In bootloader mode, the PD1 pin is not used, so you can freely connect the board via WCH-LinkE. To switch to this mode, just press a single button on the board, even while it is running.

4.EEPROM

Not quite EEPROM, and not even quite flash, but still. ATMEGA from Arduino has a special memory area that is great for storing small data.

I need about 10 bytes to store settings such as operating mode, transmission frequency and local parameters. Plus about 30 more bytes to store the username and password.

The chip's stated memory capacity is 10,000 cycles, which should be enough for the user to change settings.

The page size in ch32v003 is 64 bytes (you cannot erase less than this amount), which means one page is enough for everything, and there will still be room on the CRC.

We limit the firmware size to two pages. The second page is needed in case there is a power cut while writing to memory. In this case, we may not save new settings, but when power is restored, at least the old settings will remain in memory.

Link.ld

Link.ld

And we wrap the flash around these pages in the form of clear methods that take as input a link to config_t, which under the hood is just an array of 62 bytes (two for the flag and crc):

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

Here's an example to help you understand how it works. We create a structure with the necessary parameters and combine it with config_t through the union operator. This allows the raw object to reside in the same places in RAM as variables, making its use very intuitive.

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

We also had to work on the bootloader, since the original version completely removes the flash memory, and we needed to leave some. I decided to leave this part behind the scenes.

If you are interested, you can study the bootloader code, it is located in the tools folder as an archive. I did not analyze it in detail, since the code itself turned out to be rather collective farm, but it worked, and I did not waste time polishing it. However, the original bootloader in exe format, taken from the openwch GitHub repository, works correctly with my bootloader.

5. Port monitor

I did not bother creating bicycles, since VS Code provides the ability to add any functionality using extensions. I studied the most popular ones and chose what seemed most convenient to me. By the way, this extension has a function for saving the console to a file.

this is what the terminal looks like

this is what the terminal looks like

6. Plotter

Here, surprisingly, it’s similar. The resulting solution has much more capabilities than the Arduino plotter. It displays the current value and allows you to view logs, excluding unnecessary data from them. It is possible to separate the graphs and adjust the scale at your discretion.

To display a value, you need to start the line with the “>” symbol, pass the name of the variable and the value separated by a colon. You can pass multiple values ​​in one line, separating them with a comma.

In the example below, I connected a potentiometer to pin D4, output the value from it, with Vref (which has a constant voltage of 1.2 V) and a simple counter that increases from 300 to 1000 and resets.

printf(">D4_voltage:%d\r\n", analogRead(D4));printf(">Vref_voltage:%d\r\n", analogRead(Aref));printf(">counter:%d\r\n", counter);
this is what a plotter looks like

this is what a plotter looks like

7. Debugging with a debugger

As already mentioned in paragraph 3.1, go to CMakeLists.txt and set the debug there (pay attention to the gif). IMPORTANT!!! For debugging you need a special wch-linkE programmer; a regular usb-uart is not enough.

this is what the debug looks like

this is what the debug looks like

I won't go into debugging details, but you can verify that OpenOCD is working by looking at the value of the variable counter.

The Pessimist's Bottom Line

I have to note that there are differences from the Arduino IDE that I could not remove:

  • The bootloader must be loaded manually (or buy a board with a bootloader already installed).

  • You also need to make the cable yourself (or purchase a ready-made one).

  • To flash the firmware you need to turn off the monitor and plotter.

  • Using the printf and string library takes up a significant amount of memory.

The last point can be disabled using CMake, and then the minimum firmware with blink will occupy less than 2 kBytes. All results below for the type config Debug.

Minimal firmware

Minimal firmware

But to use it, you will have to juggle the power supply and hold down the boot every time you flash the firmware.

You can change the situation a little and allow the use of the button on the board and during firmware. This may take up another kilobyte of memory, but it will make using the function more convenient. However, it is still inconvenient.

Boot button while the program is running

Boot button while the program is running

Well, a minimal pleasant config with which you don’t need to get off the couch. It takes up only 4 kilobytes, which is a quarter of the available memory. This config does not use printf.

Firmware without pressing a button

Firmware without pressing a button

Build mode Release the latter squeezes down to 3700, so I don’t list it separately, there won’t be any miracles.

The Optimist's Bottom Line

Working with the board became so easy that I wanted to use it in all my projects. Or rather, come up with projects that could be implemented on this board.

I have always dreamed of being able to measure current consumption in the range from microamps to milliamps. To do this, I replaced the shunt on the module with INA219, expanding the measurement range to 1 µA – 30 mA. In my opinion, this is an ideal option for measuring the consumption of a board with a CH582M, but with an ESP32 this approach will no longer work. In the gif below, the consumption of a 300k resistor is measured.

Alex Hoover released a video about his new smartwatch and I wanted to play with the tape on ws2812 with hex coordinates.

The pleasure is received, the board has met all its requirements, and the project is off to conquer OpenSource.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *