Milk-V Duo Example Projects

Here I document my Milk-V Duo development board personal projects.

The Milk-V Duo is a cheap RISC-V development board you can get for around 10 euros in AliExpress. It has a powerful 1GHz processor and it runs a custom linux, but it doesn't come video ports or free USB ports to attach devices to. Almost all interactions are done through SSH connections.

Here are some places to start from when searching for information about this board:

 

Blinking LEDs with shell scripts

My first project was the usual LED blinking test. It is a simple Bourne Shell (/bin/sh) script because the current linux distro at the time of writing doesn't use bash. You can find it at my GitHub repository. Here is the script code:

 
#!/bin/sh
LED=440
# Run GPIO-LED
echo $LED > /sys/class/gpio/export
# Setup GPIO-LED
echo out > /sys/class/gpio/gpio$LED/direction
            
echo 1 > /sys/class/gpio/gpio$LED/value
sleep 0.5
echo 0 > /sys/class/gpio/gpio$LED/value
sleep 0.5
            
# cleanup gpio
echo $LED > /sys/class/gpio/unexport
 

 

Ball boucing around the screen with LED blinking

My second project was a shell script that bounces a character around the SSH screen. Remember it was written for /bin/sh and not /bin/bash, so some inconveniences had to be addressed, like I couldn't read a key with a timeout to exit the program nicely and you are forced to use CTRL-C. You can find it at my GitHub repository Here is the script code:

 
    #!/bin/sh 

    #limits of screen
    MAXX=79 
    MAXY=24
    
    #GPIO device
    LED_GPIO=/sys/class/gpio/gpio440
    
    BALLX=1
    BALLY=1
    INCX=1
    INCY=1
    KBENTER=""
    
    printBallXY () {
        local X=$1
        local Y=$2
        clear
        printf "\033[${Y};${X}f O"
    }
    
    blinkledOn () {
        echo 1 > $LED_GPIO/value
    }
    
    blinkledOff () {
        echo 0 > $LED_GPIO/value
    }
    
    #open device
    echo 440 > /sys/class/gpio/export
    echo out > $LED_GPIO/direction
    
    #the only way to stop the program is using CTRL-C
    while true
    do
        printBallXY ${BALLX} ${BALLY}
    
        if [ $((BALLX)) -gt $((MAXX)) ]; then INCX=-1; blinkledOn; fi
        if [ $((BALLY)) -gt $((MAXY)) ]; then INCY=-1; blinkledOn; fi
        if [ $((BALLX)) -lt 0 ]; then INCX=1; blinkledOn; fi
        if [ $((BALLY)) -lt 0 ]; then INCY=1; blinkledOn; fi
    
        BALLX=$(($BALLX+$INCX))
        BALLY=$(($BALLY+$INCY))
    
        sleep 0.05
        blinkledOff
    done
 

 

Mixing C and Assembler code

My third project was a small example of how to mix C language and Assembler code. You can find it at my GitHub repository under the helloasm directoy. The code is compiled using the Milk-V Duo Examples SDK. The documentation says you should install it under ubuntu 20.04 but I installed it without problems in a Linux Mint 21.3 VirtualBox Linux machine. Remember that to compile the examples you need to set the system variables in the current bash session doing the following commands:

 
cd duo-examples 
source envsetup.sh
 

Did you know that gcc can also compile assembler? I only needed to add the source assembler (.s files) to the SOURCE variable and gcc will compile them along with the .c files. Look at the following makefile:

 
TARGET=helloasm

ifeq (,$(TOOLCHAIN_PREFIX))
$(error TOOLCHAIN_PREFIX is not set)
endif

ifeq (,$(CFLAGS))
$(error CFLAGS is not set)
endif

ifeq (,$(LDFLAGS))
$(error LDFLAGS is not set)
endif

CC = $(TOOLCHAIN_PREFIX)gcc

CFLAGS += -I$(SYSROOT)/usr/include

LDFLAGS += -L$(SYSROOT)/lib
LDFLAGS += -L$(SYSROOT)/usr/lib

SOURCE = $(wildcard *.c) $(wildcard *.s)
OBJS = $(patsubst %.c,%.o,$(SOURCE))

$(TARGET): $(OBJS)
	$(CC) -o $@ $(OBJS) $(LDFLAGS)

%.o: %.c
	$(CC) $(CFLAGS) -o $@ -c $<

.PHONY: clean
clean:
	@rm *.o -rf
	@rm $(OBJS) -rf
	@rm $(TARGET)
 
Here is the assembler .s file with the code. Just three example functions that show how to deal with variables passed in functions:
 
.section .text
.globl sumfunc, powfunc, add10func


# sum function, adds two numbers
sumfunc:
    add a0, a0, a1
    ret

# power function, elevates a0 to the a1 power
powfunc:
    #
pow1:
    mv t0, a0
    addi t1, a1, -1
pow1loop:
    #
    mv t3, t0
    addi t4, a0, -1
mult1loop:
    add t0, t0, t3
    addi t4, t4, -1
    bgt t4, zero, mult1loop    
    #
    addi t1, t1, -1
    bgt t1, zero, pow1loop
    mv a0, t0
    ret

#adds 10 to a variable. a0=pointer to variable
add10func:
    ld t0, 0(a0)
    addi t0, t0, 10
    sd t0, 0(a0)
    ret
 
And here is the source .c file that calls the assembler functions and prints the result:
 
#include <stdio.h>

    //We use longs (8 bytes) and not ints (4 bytes) because the processor is 64 bit
    extern long sumfunc (long a, long b); // adds 2 numbers
    extern long powfunc (long a, long b); // elevates a to the b power
    extern void add10func (long *a); // increments variable a in 10
    
    int main() {
        printf("Hello, World!\n");
    
        printf("sumfunc(4,3)=%i\n", sumfunc(4, 3));
        printf("powfunc(3,3)=%i\n", powfunc(3, 3));
    
        long mynumber=-110; 
        printf ("mynumber before add10func: %i\n", mynumber);
        add10func (&mynumber);
        printf ("mynumber after add10func: %i\n", mynumber);
    
        return 0;
    }