In our previous article I introduced the GD32V and the Longan Nano development board. In this article I show the porting of my cooperative multithreading OS (ULWOS2) to the GD32V. We also present some examples running on the Longan Nano, which demonstrate CDC ACM and a simple CDC terminal using the onboard LCD. All examples make use of PlatformIO.
Porting ULWOS2
Adapting ULWOS2 to run on the GD32V wasn’t difficult. RISC-V cores include an internal 64-bit free running counter that can be used for this task. All we have to do is to declare a new ULWOS2_getMilliseconds function that retrieves and calculates the elapsed milliseconds based on that counter:
#elif ULWOS2_TARGET == ULWOS2_TARGET_GD32V #include "gd32vf103.h" tULWOS2Timer ULWOS2_getMilliseconds(void) { uint64_t ticks = get_timer_value(); return (ticks / (SystemCoreClock / 4000)); }
We also have to create the new target symbol (ULWOS2_TARGET_GD32V) on ULWOS2_HAL.h, which is pretty trivial.
Since ULWOS2 is does not use any Assembly and we are using GCC (which is mandatory), no further modification is needed!
Note that our examples make use of at least one extra file (system_gd32vf103.c). It is necessary for configuring the clock sources within the MCU.
ULWOS2 as a PlatformIO library
Before we proceed, I would like to share the news that since version 1.1.1, ULWOS2 is also available as a library within PlatformIO registry and it will automatically download and install the library since it is a required library for all these examples!
First ULWOS2 example on GD32V
As usual, the first thing we do when starting on a new MCU or development board is to blink an LED. The Longan Nano includes an RGB LED connected to the following I/Os:
Red | PC13 |
Green | PA1 |
Blue | PA2 |
A header file (gd32v_pjt_include.h) defines a set of macros that can one can use for setting, resetting or toggling LED state.
The code for blinking the red LED is super simple:
void thread1(void) { ULWOS2_THREAD_START(); while(1) { LEDR(0); // red LED on ULWOS2_THREAD_SLEEP_MS(50); LEDR(1); // red LED off ULWOS2_THREAD_SLEEP_MS(500); } }
I have included other examples such as a breathing RGB LED (which uses software PWM as PC13 is not connected to any timer output), a USB CDC device which echoes all data received and also sends a fixed message every 10 seconds and a fancier version of the CDC example which prints received data on the LCD (using signals).
CDC driver
GigaDevices provide several firmware examples for the GD32V. Unfortunately these examples are not very well documented and are not designed for use with PlatformIO. The API is also not very well designed. This is why I ended up modifying their driver to better suit my needs.
Basically, this is what I have modified from the original driver:
- The library stores the USB device structure locally instead of exposing it to the application. Application code can read USB connection state by using a getter;
- Single function to perform CDC communication maintenance;
- Use core tick for timing (instead of timer 2).
- Included wchar configuration (in order to example correct USB descriptor strings) and modified interface protocol to “No class specific protocol required” (see table 17 of USB CDC spec v1.1). Thanks to this Github repository for the useful information!
While working on the driver I’ve found that it is using a lot more RAM than it is necessary. I didn’t have time to investigate it but apparently the USB device structure that GigaDevice is using creates a static reference to I/O registers that is later (during init) mapped to the actual registers. The solution would be to reference the structure instead of declaring it statically which forces unnecessary memory allocation. It would be a good idea to improve that, maybe something for the future?
The basic CDC example has only two threads, one that sends a string every 10 seconds and another that echoes back all received characters:
/* Print a message on CDC device every 10 seconds */ void printEveryTenSeconds_thread(void) { ULWOS2_THREAD_START(); while(1) { ULWOS2_THREAD_SLEEP_MS(10000); cdc_print("Periodic ULWOS2 thread, 10 seconds!\n"); LEDG_TOG; // toggle green LED } }
/* Process characters received on CDC device and prints them on the LCD */ void CDC_thread(void) { ULWOS2_THREAD_START(); // wait here until USB CDC is configured while (!cdc_isConfigured()) ULWOS2_THREAD_SLEEP_MS(1); // create printing thread ULWOS2_THREAD_CREATE(printEveryTenSeconds_thread, 10); while(1) { cdc_process(); uint8_t buf[64]; uint32_t rxLength = cdc_getReceivedData(buf, 64); if (rxLength) { cdc_sendData(buf, rxLength); LEDR_TOG; // toggle red LED } // sleep for 1ms so that other threads can run ULWOS2_THREAD_SLEEP_MS(1); } }
CDC terminal with LCD
This example shows how to use ULWOS2’s signals in order to display received data on the LCD. It makes use of a signal for synchronizing the terminal thread and CDC thread.
This is how it works: terminal thread will always block and wait for SIGNAL_NEW_CHARACTER. Once this signal arrives the thread runs and takes the newCharacter and displays it (if it is printable) or performs another action (if it is a known control character). Currently known control characters are: RETURN, BACKSPACE and CTRL+D (clear the screen).
/* Process characters received on CDC device and prints them on the LCD */ void terminal_thread(void) { ULWOS2_THREAD_START(); while (1) { // always wait for a new character to arrive <span style="color: #ff0000;"><strong>ULWOS2_THREAD_WAIT_FOR_SIGNAL(SIGNAL_NEW_CHARACTER);</strong></span> // a new character arrived, prints it if printable if (newCharacter >= ' ' && newCharacter <= '~') { LCD_ShowChar(px, py, newCharacter, 0, currentColor); px += 8; } else if (newCharacter == '\b') { // this is a backspace if (px >= 8) { px -= 8; LCD_ShowChar(px, py, ' ', 0, currentColor); } } else if (newCharacter == '\r') { // this is a return if (py < 64) py += 16; px = 0; } else if (newCharacter == 4) { // this is a CTRL + D, clear the screen LCD_Clear(CL_BLACK); px = 0; py = 0; } } }
We also had to modify CDC_thread to send the signal when a single character is received:
/* CDC thread, performs CDC processing and handles received data */ void CDC_thread(void) { ULWOS2_THREAD_START(); // wait here until USB CDC is configured while (!cdc_isConfigured()) ULWOS2_THREAD_SLEEP_MS(1); // create printing and terminal threads ULWOS2_THREAD_CREATE(printEveryTenSeconds_thread, 10); ULWOS2_THREAD_CREATE(terminal_thread, 5); while(1) { cdc_process(); uint8_t buf[64]; uint32_t rxLength = cdc_getReceivedData(buf, 64); if (rxLength) { cdc_sendData(buf, rxLength); // if we receive a single character, send it to terminal thread, otherwise print the string <span style="color: #ff0000;"><strong>if (rxLength == 1) { newCharacter = buf[0]; ULWOS2_THREAD_SEND_SIGNAL(SIGNAL_NEW_CHARACTER); }</strong></span> else LCD_ShowString(0, 32, (char*)buf, CL_RED); LEDR_TOG; // toggle red LED } // sleep for 1ms so that other threads can run ULWOS2_THREAD_SLEEP_MS(1); } }
Closing
In this article we have seen some examples demonstrating how easy it is to use ULWOS2 to enable cooperative multithreading on the RISC-V/GD32V. I hope you like it and the examples can be useful for anyone playing with the GD32V or Longan Nano development board.
As usual, all the source code is available on my Github repository.