Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

BL702

Bouffalo Lab’s BL702 is a RISC-V SoC with built-in IEEE 802.15.4 and BLE 5.0 radio. The zigbee-rs BL702 backend uses FFI bindings to Bouffalo’s lmac154 C library for radio access, combined with Embassy for async operation.

Hardware Overview

SpecValue
CoreRISC-V 32-bit (RV32IMAF), 144 MHz
Flash512 KB (XIP)
SRAM128 KB (112 KB usable after cache)
RadioBLE 5.0 + IEEE 802.15.4
Targetriscv32imac-unknown-none-elf
I/OUART ×2, SPI, I2C, ADC, DAC, USB 2.0 FS

Common Modules and Boards

  • XT-ZB1 — Zigbee module based on BL702
  • DT-BL10 — compact BL702 breakout
  • Pine64 Pinenut — BL602/BL702 module
  • BL706 IoT DevBoard (Sipeed) — devkit with USB and headers

Memory Map

FLASH : ORIGIN = 0x23000000, LENGTH = 512K
RAM   : ORIGIN = 0x42014000, LENGTH = 112K

Prerequisites

Rust Toolchain

rustup default nightly
rustup update nightly

# Add the RISC-V target
rustup target add riscv32imac-unknown-none-elf

# rust-src for build-std
rustup component add rust-src

Vendor Libraries (for real radio operation)

The BL702 backend uses FFI bindings to two pre-compiled C libraries from Bouffalo’s BL IoT SDK:

  • liblmac154.a — 802.15.4 MAC/PHY library
  • libbl702_rf.a — RF calibration and configuration

These libraries are compiled with rv32imfc/ilp32f (hardware float ABI), while Rust targets riscv32imac/ilp32 (soft-float). The .a files must have their ELF float-ABI flag stripped before linking.

Flash Tool

cargo install blflash   # Community flash tool for BL702

Building

With Stubs (CI mode — no vendor SDK)

cd examples/bl702-sensor
cargo build --release --features stubs -Z build-std=core,alloc

The stubs feature provides no-op implementations of all FFI symbols, allowing the full Rust code to compile and link without the vendor libraries. This is how CI verifies the BL702 code compiles on every push.

With Real Vendor Libraries

Three options for linking vendor libraries (in priority order):

Option 1: Full SDK path

BL_IOT_SDK_DIR=/path/to/bl_iot_sdk cargo build --release -Z build-std=core,alloc

Option 2: Explicit library directories

LMAC154_LIB_DIR=/path/to/lib BL702_RF_LIB_DIR=/path/to/lib cargo build --release -Z build-std=core,alloc

Option 3: Local vendor_libs/ directory

Place ABI-patched copies of liblmac154.a and libbl702_rf.a in examples/bl702-sensor/vendor_libs/:

cargo build --release -Z build-std=core,alloc

CI Build Command

From .github/workflows/ci.yml:

cd examples/bl702-sensor
cargo build --release --features stubs -Z build-std=core,alloc

# Firmware artifact extraction
OBJCOPY=$(find $(rustc --print sysroot) -name llvm-objcopy | head -1)
$OBJCOPY -O binary $ELF ${ELF}.bin
$OBJCOPY -O ihex   $ELF ${ELF}.hex

Build Script (build.rs)

The build.rs handles vendor library discovery:

#![allow(unused)]
fn main() {
// Skip vendor libs when `stubs` feature is active (CI mode)
let use_stubs = env::var("CARGO_FEATURE_STUBS").is_ok();
if use_stubs { return; }

// Priority: BL_IOT_SDK_DIR → LMAC154_LIB_DIR → vendor_libs/
}

.cargo/config.toml

[build]
target = "riscv32imac-unknown-none-elf"

[unstable]
build-std = ["core", "alloc"]

Flashing

blflash

blflash flash target/riscv32imac-unknown-none-elf/release/bl702-sensor.bin \
    --port /dev/ttyUSB0

Bouffalo Dev Cube (GUI)

  1. Download BLDevCube from Bouffalo
  2. Select BL702 chip
  3. Load the .bin file
  4. Connect via UART and flash

Entering Boot Mode

Hold the BOOT pin low during power-on or reset to enter the UART bootloader.

MAC Backend Notes

The BL702 MAC backend lives in zigbee-mac/src/bl702/:

zigbee-mac/src/bl702/
├── mod.rs      # Bl702Mac struct, MacDriver trait impl, PIB management
└── driver.rs   # Bl702Driver — FFI bindings to lmac154.a

Feature Flag

zigbee-mac = { features = ["bl702"] }

Architecture

MacDriver trait methods
       │
       ▼
Bl702Mac (mod.rs)
  ├── PIB state (addresses, channel, config)
  ├── Frame construction (beacon req, assoc req, data)
  └── Bl702Driver (driver.rs)
         ├── FFI → liblmac154.a (Bouffalo C library)
         │     ├── lmac154_setChannel / setPanId / setShortAddr
         │     ├── lmac154_triggerTx / enableRx / readRxData
         │     └── lmac154_runCCA / getRSSI / getLQI
         ├── TX completion: lmac154_txDoneEvent callback → TX_SIGNAL
         └── RX completion: lmac154_rxDoneEvent callback → RX_SIGNAL

FFI Callbacks

The vendor library calls back into Rust through these functions:

#![allow(unused)]
fn main() {
#[no_mangle]
extern "C" fn lmac154_txDoneEvent(/* ... */) {
    TX_SIGNAL.signal(tx_status);
}

#[no_mangle]
extern "C" fn lmac154_rxDoneEvent(/* ... */) {
    // Copy RX data to static buffer
    RX_SIGNAL.signal(());
}
}

Interrupt Registration

After creating the MAC, register the M154 interrupt handler:

#![allow(unused)]
fn main() {
// bl_irq_register(M154_IRQn, lmac154_getInterruptHandler());
// bl_irq_enable(M154_IRQn);
}

Radio Features

  • Hardware CRC generation and checking
  • Configurable TX power: 0 dBm to +14 dBm
  • RSSI / LQI measurement
  • Hardware auto-ACK with configurable retransmission
  • Hardware address filtering (PAN ID, short addr, long addr)
  • CSMA-CA support
  • AES-128 CCM hardware acceleration

Example Walkthrough

The bl702-sensor example implements a Zigbee 3.0 temperature & humidity end device with button control and UART logging.

Custom Embassy Time Driver

Since there is no embassy-bl702 HAL yet, the example provides a minimal Embassy time driver using the BL702 TIMER_CH0:

#![allow(unused)]
fn main() {
// 32-bit match-compare timer at 1 MHz (FCLK/32 from 32 MHz)
const TIMER_BASE: usize = 0x4000_A500;

pub fn init() {
    // Prescaler: FCLK(32 MHz) / 32 = 1 MHz tick
    write_volatile(TCCR, 0x1F);
    // Free-running mode
    write_volatile(TMR_0, 0xFFFF_FFFF);
    // Enable counter
    write_volatile(TCER, 0x01);
}
}

UART Logging

Debug output goes through the BL702 UART peripheral:

#![allow(unused)]
fn main() {
impl log::Log for Bl702Logger {
    fn log(&self, record: &log::Record) {
        let mut w = UartWriter;
        write!(w, "[{}] {}\r\n", record.level(), record.args());
    }
}
}

GPIO Button

Direct register-based GPIO for the join/leave button:

#![allow(unused)]
fn main() {
const BUTTON_PIN: u8 = 8;  // GPIO8 on most BL702 modules
gpio::configure_input_pullup(BUTTON_PIN);
}

Troubleshooting

SymptomCauseFix
Linker error: undefined lmac154_*Vendor libraries not linkedSet BL_IOT_SDK_DIR or use --features stubs
Float ABI mismatchVendor .a uses hard-floatStrip ELF float-ABI flag from .a files
blflash can’t connectNot in boot modeHold BOOT pin low during reset
No UART outputWrong UART pins or baudCheck board schematic; default 115200 baud
Timer not workingTIMER_CH0 not initializedEnsure time_driver::init() runs before Embassy
Build fails without stubsMissing vendor lib env varsSet LMAC154_LIB_DIR or BL_IOT_SDK_DIR