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:
- Official documentation for starters
- Official forum where everybody can post their questions and share information about their projects
- RISC-V Reddit where you can post general or particular questions about RISC-V and its boards
 
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;
}