Blink LED on FreeRTOS

Hey there,
recently got my Duo and started playing around a bit. I was curious how I could blink the LED from a FreeRTOS task instead of running it in Linux.
Turns out it’s not super hard. I’m not a RTOS pro, so I just started writing to the registers and got it working. There might be better ways to do it, but I was really just curious to check out how Linux runs on one core and an IO task on the second core.

In order to make this work, follow the RTOS setup from the other threads. Specifically you have to edit freertos/cvitek/kernel/include/riscv64/FreeRTOSConfig.h and disable

#define configUSE_TICK_HOOK 0

Next edit freertos/cvitek/task/comm/src/riscv64/comm_main.c

/* Standard includes. */
#include <stdio.h>

/* Kernel includes. */
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "mmio.h"
#include "delay.h"

/* cvitek includes. */
#include "printf.h"
#include "rtos_cmdqu.h"
#include "cvi_mailbox.h"
#include "intr_conf.h"
#include "top_reg.h"
#include "memmap.h"
#include "comm.h"
#include "cvi_spinlock.h"

//#define __DEBUG__

#ifdef __DEBUG__
#define debug_printf printf
#define debug_printf(...)

 * Function prototypes
void pinMode( uint8_t port, uint8_t pin, uint8_t value );
void writePin( uint8_t port, uint8_t pin, uint8_t value );
uint8_t readPin( uint8_t port, uint8_t pin );
void app_task( void *param );

 * Global parameters
#define TOP_BASE  0x03000000

// GPIO Register base
#define XGPIO     (TOP_BASE + 0x20000 )
// GPIO Port offset
#define GPIO_SIZE 0x1000

// Port A external port register (read from here when configured as input)
#define GPIO_EXT_PORTA      0x50
// Port A data register
#define GPIO_SWPORTA_DR     0x00
// Port A data direction register
#define GPIO_SWPORTA_DDR    0x04

#define PORT_A 0
#define PORT_B 1
#define PORT_C 2
#define PORT_D 3

#define GPIO_INPUT  0
#define GPIO_OUTPUT 1
#define GPIO_LOW    0
#define GPIO_HIGH   1

#define PINMUX_BASE                        (TOP_BASE + 0x1000)
#define FMUX_GPIO_FUNCSEL_BASE             0xd4
#define FMUX_GPIO_FUNCSEL_MASK             0x7
#define FMUX_GPIO_FUNCSEL_GPIOC            3

#define FUNCSEL(port, pin) (FMUX_GPIO_FUNCSEL_BASE + port * 0x20u + pin)
#define BIT(x)             (1UL << (x))

/* mailbox parameters */
volatile struct mailbox_set_register *mbox_reg;
volatile struct mailbox_done_register *mbox_done_reg;
volatile unsigned long *mailbox_context; // mailbox buffer context is 64 Bytess

 * Function definitions
void pinMode( uint8_t port, uint8_t pin, uint8_t value ) {
		BIT( pin ), // erase Bit of PIN_NO( LED )
		BIT( pin )  // set Bit of PIN_NO( LED )

void writePin( uint8_t port, uint8_t pin, uint8_t value ) {
	uint32_t base_addr = XGPIO + GPIO_SIZE * port;

	uint32_t reg_val = mmio_read_32( base_addr + GPIO_SWPORTA_DR );
	reg_val = ( value == GPIO_HIGH ? ( reg_val | BIT(pin) ) : ( reg_val & (~BIT(pin)) ) );
	mmio_write_32( base_addr + GPIO_SWPORTA_DR, reg_val );

uint8_t readPin( uint8_t port, uint8_t pin ) {
	uint32_t base_addr = XGPIO + GPIO_SIZE * port;
	uint32_t reg_val = 0;

	// let's find out if this pin is configured as GPIO and INPUT or OUTPUT
	uint32_t func = mmio_read_32( PINMUX_BASE + FUNCSEL( port, pin ) );
	if( func == FMUX_GPIO_FUNCSEL_GPIOC ) {
		uint32_t dir = mmio_read_32( base_addr + GPIO_SWPORTA_DDR );
		if( dir & BIT( pin ) ) {
			reg_val = mmio_read_32( GPIO_SWPORTA_DR + base_addr );
		} else {
			reg_val = mmio_read_32( GPIO_EXT_PORTA + base_addr );
	} else {
		printf( "%d not configured as GPIO\n", pin );

	return( ( reg_val >> pin ) & 1 );

void app_task(void *param) {
	const TickType_t xDelay = 500 / portTICK_PERIOD_MS;

	// PortC Pin 24
	uint8_t DUO_LED_PIN = 24;
	uint8_t DUO_LED_PORT = PORT_C;
	printf( "Start blink LED on port %d, pin %d\n", DUO_LED_PORT, DUO_LED_PIN );

	// Show the pinmux (should be set to 3 to be a GPIO)
		"PINMUX[%d,%d][0x%x]: 0x%x\n",

	// set direction to output

	while( 1 ) {
		vTaskDelay( xDelay );

		vTaskDelay( xDelay );


void main_cvirtos( void ) {
	printf( "create cvi task\n" );

	/* Start the tasks and timer running. */

	xTaskCreate( app_task, "app_task", 1024, NULL, 1, NULL );


	/* If all is well, the scheduler will now be running, and the following
	line will never be reached.  If the following line does execute, then
	there was either insufficient FreeRTOS heap memory available for the idle
	and/or timer tasks to be created, or vTaskStartScheduler() was called from
	User mode.  See the memory management section on the FreeRTOS web site for
	more details on the FreeRTOS heap  The
	mode from which main() is called is set in the C start up code and must be
	a privileged mode (not user mode). */
	printf( "cvi task end\n" );

	for( ;; )

You can either just build the entire image now or what I prefer, just run the build_uboot target which will create the fip.bin. This file can simply be copied to the boot-partition on the SD card. Which is super handy since you don’t need to burn the whole SD card each time you make changes to your FreeRTOS application. I haven’t found out how to load the fip.bin during runtime. So I still run a reboot after each new file.
In order to compile just uboot (which will have FreeRTOS as a dependency) you can run

export MILKV_BOARD=milkv-duo
source milkv/

source build/
defconfig cv1800b_milkv_duo_sd


The resulting file can be found in fsbl/build/cv1800b_milkv_duo_sd/fip.bin

From here I usually login to my Duo and run the following commands to update the file in the boot partition:

mkdir /root/boot
mount /dev/mmcblk0p1 /root/boot
scp user@your-build-machine:duo-buildroot-sdk/fsbl/build/cv1800b_milkv_duo_sd/fip.bin boot/fip.bin
umount /root/boot

You should also make sure you disable the on the Linux side. I don’t think anything bad would happen, since each core would just overwrite the GPIO register. But you want to make sure FreeRTOS is in charge and not Linux:

cd /mnt/system

Let me know if this was interesting. Next I want to try to get some communication going between Linux and FreeRTOS. Looks like the mailbox system is the way to go.

Long story short I need a pulse on a gpio pin shorter than .35us. I can’t do it with wiringx so I’m exploring controlling the the gpio with registers. I’m in over my head making sense of this but I’m trying to get the concepts figured out. One thing is that tripping me up is that there are four gpio sets and one set of pins. How do I know what set to use to control a pin? I’m not really diving in to rtos yet but I’m impressed with your work.

I had some inspiration by looking into

Also appears my method of finding the FUNCSEL register doesn’t quite work for other pins, best is to check freertos/cvitek/hal/cv180x/config/cv180x_reg_fmux_gpio.h. You don’t really need that if( func == FMUX_GPIO_FUNCSEL_GPIO ) it really just checks if the pin is setup as a GPIO or if it’s configured to do its alternative functions (like I2C or UART, etc.)

As for finding out which pin lives on which port. I’ve looked into the CV180xB_QFN68_PINOUT_V0.1_CN.xlsx inside the ZIP file. There’s a tab called PIN Data Table (QFN68) which shows what function each pin has and in which port it lives.

It is also slightly confusing, since the GP25 on the breakout board refers to pin 1 on the actual CV1800B. So you’ll also have to check the schematic to know which pin on the chip goes to which GP on the board.

Final piece is: you might have to update the PINMUX of the bootloader if a pin has not yet assigned it’s function. But it’s relatively easy, just add a new line for the pin. In the end I think you could even configure it inside of FreeRTOS.

Here is a demo for your reference that allows the small core to control LEDs through mailbox:

Sweet, is there a bit more explanation what exactly happens on RTOS side? I didn’t find the source for the rtos_cmdqu … there’s only header files.

1 Like

Thanks for pointing me to the spreadsheet, I think I’m starting to get my head around figuring out what gpio set to use. One thing that has me twisted up is that in your code as well as Milk-V Duo GPIO LED Tutorial has used gpio 24 for the onboard LED. However, the example code for the C/linux SDK is using gpio 25. Is there some conversion that I need to do? Also from the sheet it’s listed as the xgpioc24 where C is referring to the 3rd gpio set and that the CV pin number is 1, AUD_AOUTR. Also that xgpioc24 is the default functiion Am I understanding that correctly? Thanks for your help.

The confusion about the ports is indeed something I stumbled over too.
There’s two port definitions: the MilkV Duo board pins and the CV180B pins.
In the Duo board they named the pin for the LED GP25. If you follow the traces in the schematic it is connected to pin 1 on the CV180B. I think you looked it up correctly, it’s some IIS (audio) bus and it is XGPIOC if you use it as a general IO port. The mapping of which function is assigned to the port is done in the PINMUX register. You can see in the sheet which value is for which function. 3 relates to general IO.
The bootloader sets this with the PINMUX() macro, but I don’t see any reason that you couldn’t change this during runtime if you wanted to change it in your code instead of the bootloader.

In case you do run a pin as a gpio you also have to tell it the direction: input/output. That’s the second config register. And depending on that setting you read or write different registers to get it set its state.

You can check how a port is configured with the duo-pinmux GP25 command. It’s already pre compiled so you can ignore the build instructions in the docs:

Again keep in mind the Duo board pins are not the same as the CV180B pins. It might be a good idea to map then out from the schematic, then you can use those names in your code.

What schematics did you find that shows the relation between the duo board and the CV180b? I can’t seem to find one, but I can’t open the OrCad file.

I think I found it and it was under my nose the whole time. I was so hung up on finding a schematic I could trace I missed the chart in Milkv-Duo Data Sheet. The pic 1.4 chapter 4 seems to be correct but it doesn’t match the table below. There is some confusion as to pin 21-23. I think the picture is right as it makes more sense.

All the schematics and datasheets are on GitHub

Thanks for all the help I’ve only been programming for a couple of weeks and still learning. I think I’ve got a handle on most of how it works, looking at the src for duo-pinmux kind of connected things for me, as far as the pinmux registers. I’m ready to start coding and see if I can get a short enough signal. The last thing I’m not sure of is that some gpios don’t seem to have a set and are listed a PWRGPIO. Take for example the board GP2 goes to SD1_GPIO1 on the chip. It only has three available options, a uart, PWM, and pwr gpio[26]. I could avoid these pins as there of plenty of available gpio pins but I still wonder what they do. Any way your help and code have been a huge help.

same question, milk-v duo 256 board.
it seems, GPIOA corresponds to GPIO0, GPIOB is GPIO1, GPIOC is GPIO2, and PWR_GPIO is GPIO3, but i need someone knowledgable to confirm this theory)