HiFive-1 Bare Bones
WAIT! Have you read Getting Started, Beginner Mistakes, and some of the related OS theory? |
This page or section refers to its readers or editors using I, my, we or us. It should be edited to be in an encyclopedic tone. |
This tutorial needs to explain what the code does as tutorials are not just copy paste. You can help out by editing this page to include more context to what the code does. |
Difficulty level |
---|
Medium |
If you are looking to get a RISC-V board, the HiFive1 is a relatively easy (albeit slightly expensive at US$69) way to get a RISC-V board. This Bare Bones tutorial is an attempt at bringing up the HiFive-1 board and getting ready to start some OS development as you would on most other platforms.
Prerequisites
You'll need a few pieces of software:
- Git for acquiring the SDK
- build-essential, bison, flex for building the SDK
- minicom (or screen) for checking on the serial of the HiFive-1 board
Acquiring the SDK
First, in order to compile for the HiFive-1 Board, you need to download and build the SDK:
$ git clone --recursive https://github.com/sifive/freedom-e-sdk.git
$ cd freedom-e-sdk
$ make tools
The SDK builds a bunch of stuff we do not really need (including a Newlib port that lets the developer write basic C programs and run it bare-metal). Unfortunately, I did not yet invest the time to turn off those unneeded features, so for now we will need to wait for a full build which does take some time. Grab a tea or coffee.
The Bare Bones
This Bare Bones tutorial will setup the 16Mhz Crystal for UART timing, then initialize the UART and finally print a nice message to the serial port.
The kernel source code
First, create a file named main.c which will serve as both the entry point from the firmware and the starting point for the kernel you will be writing.
#include <stdint.h>
#include <stddef.h>
/* GPIO */
#define GPIO_CTRL_ADDR 0x10012000UL
#define GPIO_IOF_EN 0x38
#define GPIO_IOF_SEL 0x3C
#define IOF0_UART0_MASK 0x00030000UL
/* UART */
#define UART0_CTRL_ADDR 0x10013000UL
#define UART_REG_TXFIFO 0x00
#define UART_REG_RXFIFO 0x04
#define UART_REG_TXCTRL 0x08
#define UART_REG_RXCTRL 0x0c
#define UART_REG_IE 0x10
#define UART_REG_IP 0x14
#define UART_REG_DIV 0x18
#define UART_TXEN 0x1
/* PRCI */
#define PRCI_CTRL_ADDR 0x10008000UL
#define PRCI_HFROSCCFG (0x0000)
#define PRCI_PLLCFG (0x0008)
#define ROSC_EN(x) (((x) & 0x1) << 30)
#define PLL_REFSEL(x) (((x) & 0x1) << 17)
#define PLL_BYPASS(x) (((x) & 0x1) << 18)
#define PLL_SEL(x) (((x) & 0x1) << 16)
/* This function will read a 32-bit value from an MMIO register */
static inline uint32_t
mmio_read_u32(unsigned long reg, unsigned int offset)
{
return (*(volatile uint32_t *) ((reg) + (offset)));
}
/* This function will write a byte to an MMIO register */
static inline void
mmio_write_u8(unsigned long reg, unsigned int offset, uint8_t val)
{
(*(volatile uint32_t *) ((reg) + (offset))) = val;
}
/*This function will write a 32-bit value to an MMIO register */
static inline void
mmio_write_u32(unsigned long reg, unsigned int offset, uint32_t val)
{
(*(volatile uint32_t *) ((reg) + (offset))) = val;
}
/* Initialize the UART */
static void
uart_init()
{
/* These two writes enable the UART via the GPIO */
mmio_write_u32(GPIO_CTRL_ADDR,
GPIO_IOF_SEL,
mmio_read_u32(GPIO_CTRL_ADDR, GPIO_IOF_SEL)
& ~IOF0_UART0_MASK);
mmio_write_u32(GPIO_CTRL_ADDR,
GPIO_IOF_EN,
mmio_read_u32(GPIO_CTRL_ADDR, GPIO_IOF_EN)
| IOF0_UART0_MASK);
/*
* Assuming a 16Mhz Crystal (which is Y1 on the HiFive1), the divisor
* for a 115200 baud rate is 138
*/
mmio_write_u32(UART0_CTRL_ADDR, UART_REG_DIV, 138);
mmio_write_u32(UART0_CTRL_ADDR,
UART_REG_TXCTRL,
mmio_read_u32(UART0_CTRL_ADDR, UART_REG_TXCTRL)
| UART_TXEN);
/* busy loop until the line is asserted... */
volatile int i = 0;
while(i++ < 1000000);
}
/* Transmit a single byte over the UART */
static void
__uart_write(uint8_t byte)
{
/* wait for the UART to become ready */
while (mmio_read_u32(UART0_CTRL_ADDR, UART_REG_TXFIFO) & 0x80000000)
;
/* write to the UART transmit FIFO */
mmio_write_u8(UART0_CTRL_ADDR, UART_REG_TXFIFO, byte);
}
/* Transmit a buffer of length "len" over the UART */
static void
uart_write(uint8_t *buf, size_t len)
{
int i;
for (i = 0; i < len; i ++) {
__uart_write(buf[i]);
/* If an LF was written, also write a CR */
if (buf[i] == '\n') {
__uart_write('\r');
}
}
}
/* People, the simplest ever strlen function */
static size_t
strlen(char *str)
{
int len = 0;
int i;
for (i = 0; str[i] != 0; i ++)
len ++;
return len;
}
/* Write a null-terminated string to the UART, transmitting it */
static void
uart_write_string(uint8_t *buf)
{
uart_write(buf, strlen((char *) buf));
}
/* Initialize the clock source for the UART, in this case the 16MHz crystal */
static void
prci_init(void)
{
/* Make sure the HFROSC is on */
mmio_write_u32(PRCI_CTRL_ADDR, PRCI_HFROSCCFG,
mmio_read_u32(PRCI_CTRL_ADDR, PRCI_HFROSCCFG)
| ROSC_EN(1));
/* Run off 16 MHz Crystal for accuracy */
mmio_write_u32(PRCI_CTRL_ADDR, PRCI_PLLCFG,
mmio_read_u32(PRCI_CTRL_ADDR, PRCI_PLLCFG)
| (PLL_REFSEL(1) | PLL_BYPASS(1)));
mmio_write_u32(PRCI_CTRL_ADDR, PRCI_PLLCFG,
mmio_read_u32(PRCI_CTRL_ADDR, PRCI_PLLCFG)
| (PLL_SEL(1)));
/* Turn off HFROSC to save power */
mmio_write_u32(PRCI_CTRL_ADDR, PRCI_HFROSCCFG,
mmio_read_u32(PRCI_CTRL_ADDR, PRCI_HFROSCCFG)
& ~(ROSC_EN(1)));
}
/* The entry point */
void
main(void)
{
prci_init();
uart_init();
uart_write_string("Hello, world!\nThis is myOS on the HiFive-1 Board!\n");
/* For now, just halt */
for (;;);
}
/* The _actual_ entry point, this is then fixated to 0x20400000 via the linker script */
__attribute__((section(".init")))
void
_start(void)
{
main();
}
Next, we need to do some linker script magic to make sure that our code is positioned where it should be. It's worth noting that the firmware seems to perform an unconditional jump to 0x204000000 and expects the application to continue from there. Thus, in the above code we place the _start() function into a special section (aptly named, ".init") which we will move to the correct address via the linker script:
OUTPUT_ARCH("riscv")
ENTRY(_start)
MEMORY
{
flash (rxai!w) : ORIGIN = 0x20400000, LENGTH = 512M
ram (wxa!ri) : ORIGIN = 0x80000000, LENGTH = 16K
}
PHDRS
{
flash PT_LOAD;
ram_init PT_LOAD;
ram PT_NULL;
}
SECTIONS
{
__stack_size = DEFINED(__stack_size) ? __stack_size : 2K;
.init :
{
KEEP (*(SORT_NONE(.init)))
} >flash AT>flash :flash
.text :
{
*(.text .text.*)
} >flash AT>flash :flash
.fini :
{
KEEP (*(SORT_NONE(.fini)))
} >flash AT>flash :flash
.rodata :
{
*(.rdata)
*(.rodata .rodata.*)
} >flash AT>flash :flash
. = ALIGN(4);
.data :
{
*(.data .data.*)
. = ALIGN(8);
PROVIDE( __global_pointer$ = . + 0x800 );
*(.sdata .sdata.*)
*(.gnu.linkonce.s.*)
. = ALIGN(8);
*(.srodata.cst16)
*(.srodata.cst8)
*(.srodata.cst4)
*(.srodata.cst2)
*(.srodata .srodata.*)
} >ram AT>flash :ram_init
. = ALIGN(4);
PROVIDE( _edata = . );
PROVIDE( edata = . );
PROVIDE( _fbss = . );
PROVIDE( __bss_start = . );
.bss :
{
*(.bss .bss.*)
*(COMMON)
. = ALIGN(4);
} >ram AT>ram :ram
. = ALIGN(8);
PROVIDE( _end = . );
PROVIDE( end = . );
.stack ORIGIN(ram) + LENGTH(ram) - __stack_size :
{
PROVIDE( _heap_end = . );
. = __stack_size;
PROVIDE( _sp = . );
} >ram AT>ram :ram
}
Now this linker script also provides a few extra symbols that can come handy when developing your OS. In particular, it gives your OS a stack location!
Now that we have the kernel source code and we have a linker script, what remains is building our image. First, however, let's setup a few environment variables:
$ export SDK=/the/location/where/your/freedom/sdk/lies
$ export SDK_PREFIX=$(SDK)/work/build/riscv-gnu-toolchain/riscv64-unknown-elf/prefix/bin
$ export CROSS=$(SDK_PREFIX)/riscv64-unknown-elf-
$ export OPENOCD=$(SDK)/work/build/openocd/prefix/bin/openocd
Modify the $SDK variable to where your SDK is cloned to.
Finally, we can now proceed to build our image, simply:
$ ${CROSS}gcc main.c \
-g \
-march=rv32imac \
-mabi=ilp32 \
-mcmodel=medany \
-o main.img \
-T linker.lds \
-nostartfiles
Before you execute that command, you might be wondering what those options are.
- "-g" turns on debug symbols and additional debugging information. This can be useful for your kernel and (I believe) you can also the on-chip debugger to jump to functions, inspect variables, as you would in GDB.
- "-march=rv32imac" specifies the available instructions for the board. The HiFive-1 is a 32-bit RISC-V board, with the IMAC extensions.
- "I" refers to Integer Base Instructions
- "M" refers to Integer Multiplication and Division Instructions
- "A" refers to atomic instructions
- "C" refers to compressed instructions (think Thumb mode on ARM)
- "-mabi=ilp32" specifies the ABI of the platform. "ilp32" means that Integers, Longs, Pointers are 32-bit wide.
- "-mcmodel=medany" tells GCC to compile for a Medium/Anywhere code model.
The rest of the arguments should be familiar from other programming activities.
At this point, you should have a main.img in your directory and that is kernel that we have just compiled!
Testing on real hardware
To test on real hardware, you will need to first plug your HiFive-1 board to your computer of choice (preferably the one with the freshly compiled main.img from the previous steps. :-) ).
Before you can flash the kernel to the board, however, you will need an OpenOCD specification for the said board. You can find one for the HiFive-1 board here. (I am unable to directly include it, because its license is not CC0-compatible, thus you'll need to grab it yourself).
Once you have this, the process of flashing an image to the board is relatively quick:
$ ${OPENOCD} \
-f openocd.cfg \
-c "flash protect 0 64 last off; \
program main.img verify;\
resume 0x20400000;\
exit"
In this command, we first turn off the board's flash protection, program the board with the kernel and verify that the hashes match, and resume from the starting point. Finally, we exit the on-chip debugger.
If all went well, you are done and the UART (which you can observe via minicom) will now output the following:
Hello, world! This is myOS on the HiFive-1 Board!