Skip to content

samblenny/baochip-sdk

Repository files navigation

Baochip SDK

This is a baremetal SDK for the Baochip Dabao Bao1x evaluation board. Most of the work here is about peripheral drivers, linker scripts, firmware blob signing, and examples to demonstrate driver usage. The strategy here is to do drivers in no_std Rust for safety and make the public API usable from both Rust and C for compatibility.

CAUTION: This is a proof of concept. I make no promises about maintaining this code nor implementing missing features. The point of this is to document how to do bare metal stuff with the bao1x. Feel free to chop this up for parts and use it in your own projects.

Roadmap

  1. ✅ Implement Rust drivers and usage examples for timers, GPIO, and UART.

  2. ✅ Add a C FFI layer over the drivers.

  3. ✅ Get MicroPython working with the minimal set of drivers.

  4. Possible maybe future stuff:

    • Convert Makefile rules for C library stuff to use the xpack toolchain
    • Add USB CDC-ACM serial support
    • Experiment with making rv32e apps for the BIO cores (LED drivers?)
    • Write more docs
    • More USB stuff like maybe CDC-NCM networking with an IP stack

Dev Environment Setup

These instructions are for Debian 13 (Trixie), but they should probably work with minimal modifications on whatever the latest Ubuntu LTS release is. You're on your own for adapting to other Linux flavors, Windows, or macOS.

  1. Install Rust using the rustup install procedure at: rust-lang.org/learn/get-started/

    If you're allergic to piping curl into a shell, download the script on your own first and give it a read before you run it. You might also consider selecting the script's "Customize installation" option and telling it not to automatically modify your PATH, then modify PATH manually yourself.

  2. Install the riscv32imac-unknown-none-elf target with rustup:

    rustup target add riscv32imac-unknown-none-elf
    
  3. Install gcc rv32 compiler, binutils, and picolibc:

    sudo apt install binutils-riscv64-unknown-elf \
      gcc-riscv64-unknown-elf \
      picolibc-riscv64-unknown-elf
    

    Picolibc includes are at /usr/lib/picolibc/riscv64-unknown-elf/include, and /usr/lib/picolibc/riscv64-unknown-elf/lib/release/rv32imac/ilp32 has the release builds of the libraries. This is for Debian. YMMV for Ubuntu.

Build libbaochip_sdk.a Library

To build the library file for linking with C projects, do this:

$ cd code/baochip-sdk/
$ make
building target/riscv32imac-unknown-none-elf/release/libbaochip_sdk.a
cargo clean
     Removed 15 files, 16.4MiB total
cargo build --lib --release
   Compiling baochip-sdk v0.1.0 (/home/sam/code/baochip-sdk)
    Finished `release` profile [optimized] target(s) in 0.19s

You'll need to #include "baochip_sdk.h" in your C code. Also be sure to add a gcc -I<path> argument with the correct relative path from your Makefile to baochip_sdk.h in the root of this repo.

Building the Examples

This uses a Makefile to orchestrate cargo build along with some llvm tools for post build binary manipulation and a couple Python scripts to sign and UF2 pack the firmware blob.

examples/hello_c.c (C linked to Rust)

This one uses C for the application logic (hello world) and Rust for the hardware drivers. The C code has a hello world that gets linked with picolibc and a Rust wrapper. The Rust code initializes the hardware, provides a UART driver, and calls the main() exported by the C library. The C code is at examples/hello_c.c.

$ make hello_c
cargo clean
     Removed 43 files, 17.8MiB total
mkdir -p target/riscv32imac-unknown-none-elf/debug/examples
---
# Compiling C code...
riscv64-unknown-elf-gcc \
	-I/usr/lib/picolibc/riscv64-unknown-elf/include -march=rv32imac -mabi=ilp32 \
	-I. \
	-c examples/hello_c.c \
	-o target/riscv32imac-unknown-none-elf/debug/examples/hello_c.o
---
# Archiving C library...
riscv64-unknown-elf-ar rcs \
	target/riscv32imac-unknown-none-elf/debug/examples/libhello_c.a \
	target/riscv32imac-unknown-none-elf/debug/examples/hello_c.o
---
# Building Rust SDK library (libbaochip_sdk.a)...
cargo build --lib
   Compiling baochip-sdk v0.1.0 (/home/sam/code/baochip-sdk)
    Finished `dev` profile [optimized] target(s) in 0.19s
---
# Linking C library with Rust library...
riscv64-unknown-elf-gcc \
	-march=rv32imac -mabi=ilp32 -nostartfiles -nostdlib \
	-Tlink.x \
	-Wl,--gc-sections \
	-o target/riscv32imac-unknown-none-elf/debug/examples/hello_c.elf \
	target/riscv32imac-unknown-none-elf/debug/libbaochip_sdk.a \
	target/riscv32imac-unknown-none-elf/debug/examples/libhello_c.a \
	/usr/lib/picolibc/riscv64-unknown-elf/lib/release/rv32imac/ilp32/libc.a \
	-lgcc
---
# Extracting loadable sections to .bin file:
llvm-objcopy -O binary hello_c.elf hello_c.bin
---
# Signing .bin file:
binary payload size is 22408 bytes
Signed firmware blob written to target/riscv32imac-unknown-none-elf/debug/examples/hello_c.img
---
# Packing signed blob as UF2:
signed blob file size is 23176 bytes
UF2 image written to target/riscv32imac-unknown-none-elf/debug/examples/hello_c.uf2
---
cp target/riscv32imac-unknown-none-elf/debug/examples/hello_c.uf2 examples/

Copy and Run UF2 File on macOS

Once you have a signed and packed UF2 file, you can copy and run it like this (on macOS; YYMV for Linux):

$ cp hello_c.uf2 /Volumes/BAOCHIP && sync
$ diskutil unmountDisk /dev/disk4   # assumes /Volumes/BAOCHIP was /dev/disk4s1
Unmount of all volumes on disk4 was successful

After the disk is unmounted, press the Dabao PROG button to run the code (assuming your dabao bootloader is configured with bootwait enabled).

examples/blinky.rs

See comments in examples/blinky.rs for more details on what this does.

$ make blinky
cargo clean
     Removed 43 files, 17.8MiB total
cargo build --example blinky
   Compiling baochip-sdk v0.1.0 (/home/sam/code/baochip-sdk)
    Finished `dev` profile [optimized] target(s) in 0.32s
objdump -h target/riscv32imac-unknown-none-elf/debug/examples/blinky

target/riscv32imac-unknown-none-elf/debug/examples/blinky:     file format elf32-little

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .firmware     00001cf0  60060300  60060300  00000300  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000010  61000000  60061ff0  00002000  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000020  61000010  61000010  00002010  2**2
                  ALLOC
---
# Checking .data section LMA (FLASH) and VMA (RAM) addresses:
llvm-objdump -t blinky | grep _data
60061ff0 g       *ABS*	00000000 _data_lma
61000000 g       .data	00000000 _data_vma
00000010 g       *ABS*	00000000 _data_size
---
# Extracting loadable sections to .bin file:
llvm-objcopy -O binary blinky blinky.bin
---
# Signing .bin file:
binary payload size is 7424 bytes
Signed firmware blob written to target/riscv32imac-unknown-none-elf/debug/examples/blinky.img
---
# Packing signed blob as UF2:
signed blob file size is 8192 bytes
UF2 image written to target/riscv32imac-unknown-none-elf/debug/examples/blinky.uf2
---
cp target/riscv32imac-unknown-none-elf/debug/examples/blinky.uf2 examples/

Pinout & Electrical Ratings

  • Bunnie says the GPIO uses a 22 nm cell that can do 3.3V IO with 12mA current drive and 2kV HBM ESD protection.

  • GPIO outputs can do Pull-Up, but pull-down is not supported.

  • ️⚡️🔥☠️ DO NOT USE 5V. IO is not 5V tolerant.

  • Dabao v3 schematic: github.com/baochip/dabao/blob/main/dabao_v3c.pdf

  • Bootloader (boot1) serial console: TX=PB14, RX=PB13, 1M baud 8N1
    (source README-baochip.md)

Division and Precision Timing

Notes from Bunnie on relative cost of math operations on the main CPU:

  • Division takes 34 cycles
  • Shifts use a barrel shifter, but there's a potential for data dependencies to slow the pipeline. So, shift may take 1 or 2 cycles.

Tracking time is really easy with TICKTIMER if you only need a 1 ms tick for the counter. But, it's potentially desirable to have separate µs and ms time functions available for fine and coarse resolution timing.

  1. If you use a 1 MHz tick, getting 1 µs timer resolution 64-bit counter is just a matter of two register reads, a shift, and an or. But, to get a 1 ms timestamp, you'd need to do a / 1000. Division is slow, so it would be nice to avoid that overhead.

  2. If you use a 1/1024 ms tick (1.024 MHz), you can calculate ms as

    let ms = tick >> 10;
    

    and µs as

    let us = (tick * 1000) >> 10;   // or simplify fraction: (tick * 125) >> 7
    

    In both cases, you can avoid the division. You can also use ticks directly if you don't need the time of precise delays to be specified in SI units.

UART Serial Console

For UART serial debug output, you need a serial monitor that can do 1Mbaud. On Linux, this is easy. But, on macOS, you need a serial monitor that uses the IOSSIOSPEED ioctl to set non-standard baud rates (screen won't work). Homebrew's picocom reportedly works. My hbaud works. The paid "Serial" app on the App Store might work. NOTE: This is for UART serial. USB CDC is different.

To read debug serial port log messages:

  1. Solder 2.54mm header pins to your Dabao so you can put it in a breadboard.

  2. Find a fast USB serial adapter that supports 1 Mbps. Adapters using FTDI, CP2102, or CP2102N chips are a good bet. The Raspberry Pi Debug Probe seems to work.

  3. Wire up your serial adapter:

    FTDI Adapter Dabao
    TX PB13 (RX)
    RX PB14 (TX)
    GND GND
    Pi Debug Probe Dabao
    Orange Wire (TX) PB13 (RX)
    Yellow Wire (RX) PB14 (TX)
    Black Wire GND
  4. Connect the adapter, find its device with ls /dev/tty* or whatever, then start a serial monitor with screen -fn $ADAPTER_TTY 1000000 or whatever.

  5. Connect Dabao to USB. You should see boot0 console up and so on.

Boot Messages Example: USB Host

A typical boot when plugged into a USB host (computer) might look like this:

boot0 console up

~~boot0 up! (v0.9.16-1881-g3e4b0b657)~~

boot0 console up

~~boot0 up! (v0.9.16-1881-g3e4b0b657)~~

boot1 udma console up, CPU @ 350MHz!

~~Boot1 up! (v0.9.16-2542-g944a8082e: Towards Beta-0)~~

Configured board type: Dabao
Boot bypassed because bootwait was enabled
USB device ready
USB is connected!

Note the USB is connected! on the last line.

Boot Messages Example: USB Power Only

A typical boot when only connected to USB power might look like this:

boot0 console up

~~boot0 up! (v0.9.16-1881-g3e4b0b657)~~

boot0 console up

~~boot0 up! (v0.9.16-1881-g3e4b0b657)~~

boot1 udma console up, CPU @ 350MHz!

~~Boot1 up! (v0.9.16-2542-g944a8082e: Towards Beta-0)~~

Configured board type: Dabao
Boot bypassed because bootwait was enabled
USB device ready

Note the absence of USB is connected! for the last line. Here, the bootloader shell is bound to the UART port. If you press the enter key, you should see something like:


Command not recognized:
Commands include: reset, echo, altboot, boot, bootwait, idmode, localecho, uf2, boardtype, audit, lockdown, paranoid, self_destruct

Then, if you type audit + enter, you should see something like this:

audit
Board type reads as: Dabao
Boot partition is: Ok(PrimaryPartition)
Semver is: v0.9.16-2542-g944a8082e
Description is: Towards Beta-0
Device serializer: xxxxxxxx-xxxxxxxx-xxxxxxxx-xxxxxxxx
Public serial number: xxxxxx
UUID: xxxxxxxx-xxxxxxxx-xxxxxxxx-xxxxxxxx
Paranoid mode: 0/0
Possible attack attempts: 0
Revocations:
Stage       key0     key1     key2     key3
boot0       enabled  enabled  enabled  enabled
boot1       enabled  enabled  enabled  enabled
next stage  enabled  enabled  enabled  enabled
Boot0: key 1/1 (bao2) -> 60000000
Boot1: key 3/3 (dev ) -> 60020000
Next stage: key 3/3 (dev ) -> 60060000
== BOOT1 FAILED PUBKEY CHECK ==
== IN DEVELOPER MODE ==
== BOOT1 REPORTED PUBKEY CHECK FAILURE ==
In-system keys have been generated
** System did not meet minimum requirements for security **

USB CDC Serial Console

The Baochip bootloader and baremetal app both provide a simple shell interface. The boot log messages always go to the UART serial port, but the shell binding depends on whether the USB port is connected to power only or to a USB host:

  • Connected to Power Only: Shell binding goes to UART serial port. In this case, the last UART log line is USB device ready (not ...connected!).

  • Connected to Computer (USB host): Shell binding goes to USB CDC serial port. In this case, the last UART log line should be USB is connected!.

Docs, Refs, and Downloads

Dabao Board (Bao1x dev board)

Bao1x Chip (CSP Package)

These notes and documentation links should help understand peripheral IP blocks used in the Bao1x SoC. Much of this came from the baochip discord server's not-rust channel.

  • Baochip website with various docs links: baochip.com

  • uDMA block is from Pulp platform from University of Bologna/ETH Zurich:
    docs, sample drivers

    uDMA provides UART, I2C, Camera, SPI, SDIO, and ADC peripheral access by DMA

  • IRQArray is meant to be an alternative to NVIC that will work better with virtual memory. IRQs are split into banks of 16 with each bank getting a separate page of memory. This allows drivers to have their own memory spaces.

  • CPU cluster register group: docs

    These RV32 registers provide access to IRQArray, timers, and suspend/resume features.

  • Peripheral cluster register group: docs

    These RV32 registers provide access to math/crypto accelerators, TRNG, UDMA, BIO, and a variety of other as yet somewhat mysterious peripherals.

  • PLL programming is not well documented yet. Best reference is the rust source. But, the bootloader sets up the clocks and the UART, so this might not matter unless you need to do something like adjust the main clock to hit a specific I2S bit clock rate.

  • Bare metal linker file:
    xous-core/baremetal/src/platform/bao1x/link.x

  • Bare metal stack pointer, trap, and entry point setup:
    xous-core/baremetal/src/asm.rs

  • Existing BIO examples depend on Xous APIs. There isn't much documentation yet on how to use it outside of Xous (as of Jan 10, 2026). BIO is relevant for handling timing sensitive IO like I2S, 1-wire, or LED string drivers.

    Current no_std BIO drivers: xous-core/libs/bao1x-hal/src/bio_hw.rs

Notes from Xous Docs

These have some useful info for dev environment setup along with building, signing, and flashing apps for Baochip:

  1. https://betrusted.io/xous-book/ch01-02-hello-world.html
  2. https://github.com/betrusted-io/xous-core/blob/main/README-baochip.md

Binary Signing and UF2 Notes

The bao1x bootloader expects binary images to be ed25519ph signed with using the developer key that's available at betrusted-io/xous-core/devkey/dev.key.

The xous-core repo includes tools to go from ELF binary, to signed ELF binary, to bootloader-ready UF2 file. These scripts and tools from xous-core are involved in signing and UF2 creation:

  • Windows PowerShell script to sign and deploy official build artifacts for Dabao using a hardware signing token:
    xous-core/baosign.ps1

  • This tool converts .img pre-sign binaries (derived from ELF binaries) into UF2 encoded signed binaries. There's some moderately complicated stuff going on with wrapping the object code in a file structure that includes public keys, padding, version info, the signature, and maybe a couple other things.
    xous-core/tools/src/sign_image.rs (library functions)
    xous-core/tools/src/bin/sign_image.rs (command)

    The public keys that get embedded in the signed output image come from:
    xous-core/libs/bao1x-api/src/*.rs

    The developer key private key PEM comes from:
    xous-core/devkey/dev.key

    The developer key public key certificate PEM comes from:
    xous-core/devkey/dev-x509.crt

  • This section of the Xous xtask tool shows the command line arguments that it uses when invoking sign_image.rs to sign a baremetal binary:
    xous-core/xtask/src/builder.rs#L978-L1000

    That ends up doing the equivalent of:

     target/debug/sign-image --loader-image \
     target/riscv32imac-unknown-none-elf/release/baremetal-presign.img \
     --loader-key devkey/dev.key --loader-output \
     target/riscv32imac-unknown-none-elf/release/baremetal.img \
     --min-xous-ver v0.9.8-791 --sig-length 768 --with-jump --bao1x \
     --function-code baremetal
    
  • This will convert an ELF file to a pre-sign object (.img file):

     target/debug/copy-object \
     xous-core/target/riscv32imac-unknown-none-elf/release/baremetal \
     target/riscv32imac-unknown-none-elf/release/baremetal-presign.img --bao1x
    

    You can get a help message for copy-object from xous-core by doing:

    cargo run --package tools --bin copy-object
    

Build & Run sign_image.rs

  1. Install rust with rustup

  2. Clone xous-core:

    git clone --depth 100 https://github.com/betrusted-io/xous-core.git
    
  3. Build sign-image:

    cargo run -p tools --bin sign-image
    
  4. Run it:

    target/debug/sign-image --help
    

Alternate Signing Method

My signer.py script implements a functionally equivalent signing operation to sign-image invoked with the baremetal bao1x options listed above. The point of my signer script is to make it possible to sign without pulling in xous-core as a dependency.

The signer requires that you convert the ELF file to a binary blob of machine code in the style of copy-object. My Makefile does the ELF to machine code extraction step using llvm-objcopy, with some help from the linker script.

Understanding the Blob Format and Early Boot

The pre-sign image created by copy-object is meant to be copied to RRAM (the Baochip equivalent of flash) for XIP (execute in place) access by the CPU. The blob contains:

  1. A header, beginning with a jump instruction, that's sufficient to reconstruct statics from the .data section. This is a method to compress .data, which typically has many zero bytes. This matters particularly for the bootloaders, which are space constrained to fit in small ReRAM slots.

  2. The .text section with executable code (assembly and compiled rust code). This begins with init code to set up hardware, prepare the .data section in SRAM, zero the .bss section in SRAM, configure interrupt and trap handlers, set the stack pointer, and jump to _start.

  3. The .rodata section with read-only data that stays in ReRAM (flash).

For my purposes, compressing .data is probably not necessary. But, my code in .text will still be responsible for initializing .data and .bss by some means.

If using objcopy to create a blob instead of copy-object, it will be important to ensure section start offsets within the blob file stay consistent with the LMA addresses in the ELF binary. This may involve padding. Making sure the blob gets written to the correct RRAM start address happens by setting the offset in the UF2 file (see signer.py or sign_image.rs).

For link script info and .data initialization details, see:

For early hardware setup, including IRQs, see:

Bunnie says, when the Bao1x bootloader jumps to the initial JAL instruction at the start of the signed blob in ReRAM, interrupts are guaranteed to be off. Also, at that point, the UDMA UART baud rate, buffers, and clocks will be set up and ready to use (but it's best to re-initialize them anyway).

UDMA Notes

Some relevant UDMA UART code snippets from xous-core:

CAUTION: The UDMA engine expects its source data to come from the IFRAM buffers, which are outside the regular SRAM. The IFRAM address space is a totally different thing, a 256kB region of buffers mapped to its own address range starting at 0x50000000.

IFRAM address details:

pub const HW_IFRAM0_MEM:     usize = 0x50000000;
pub const HW_IFRAM0_MEM_LEN: usize = 131072;  // 128 kB
pub const HW_IFRAM1_MEM:     usize = 0x50020000;
pub const HW_IFRAM1_MEM_LEN: usize = 131072;  // 128 kB

The only way to send anything on the UDMA UART is to set up a DMA transaction. So, you have to set up an unsafe buffer in IFRAM, copy your data there, then start a UDMA transaction to read from the buffer.

USB Openmoko PIDs

Baochip has the following Openmoko USB VID PID pairs (see openmoko-usb-oui pull request):

VID PID Description
0x1d50 0x6196 Baochip-1x SoC bootloader
0x1d50 0x6197 Dabao breakout board for Baochip-1x
0x1d50 0x6198 Baosec security token

About

Board support SDK for Baochip Dabao Bao1x evaluation board

Resources

License

Stars

Watchers

Forks

Contributors