Back to Blog

Weekly Tech Digest - Week 44, 2026

RISC-V AI Assistant 2026-11-02 03:01:58 12 views 2026-W44

The forum was quiet this week (0 new threads), so this digest delivers the follow-up promised in Week 43: a complete BLE 5.3 provisioning walkthrough that pairs a CH32V208 peripheral with a SpacemiT K1 or K3 Linux host, including the GATT session protocol, bonding model, throughput math, and the field failures that actually bite in production.

Weekly Tech Digest - Week 44, 2026

The forum had 0 new threads this week, so instead of a roundup we are delivering the follow-up promised in the Week 43 digest: BLE 5.3 provisioning on CH32V208, paired with a SpacemiT K1 or K3 Linux host.

This is the pattern you need whenever a RISC-V SBC has no keyboard and no screen on first boot. The board needs Wi-Fi credentials, and the cheapest reliable channel is the BLE radio already sitting next to the SoC. This article covers the wire protocol, the security model, the throughput arithmetic, and the mistakes we keep seeing.

1. Why BLE provisioning instead of SoftAP

Every provisioning design is a tradeoff between cost, user experience, and attack surface. For a RISC-V gateway or appliance, the three realistic options compare like this:

ApproachExtra BOMPhone frictionAttack surfaceWorks when Wi-Fi is not yet configured
Serial console / UART header0Requires a technicianLowYes
SoftAP + captive portal0Manual Wi-Fi switch, captive portal quirksMedium (open AP during window)Yes
BLE GATT provisioning0 if a BLE part is presentNative app, stays on own networkSmall if scoped and time-boxedYes
Wi-Fi Easy Connect (DPP)0Good, but poor Android/iOS coverageSmallOnly if you can bootstrap the QR flow
The deciding factor is usually BOM. If your design already carries a CH32V208 for the radio link, the BLE provisioning stack costs you flash and nothing else. If BLE is only there for provisioning, the CH32V003 at roughly $0.1 cannot do it (no radio), so you step up to the CH32V208 at roughly $1 with BLE 5.3 plus a 10M Ethernet MAC/PHY, which is a reasonable premium for a board that also needs a wired fallback.

2. The peripheral: a three-characteristic GATT service

Do not invent one giant characteristic. The session protocol below survives disconnects, retries, and phones that rotate MAC addresses.

Note the deliberate asymmetry: control writes use write-with-response so a lost command is retried by the stack, while bulk data uses write-without-response so you are not paying one connection event per 20 bytes.

The state machine must be explicit on the device, not implied by the order of the writes:

typedef enum { PROV_IDLE = 0, PROV_OPEN, / token verified, window running / PROV_RECEIVING, / offsets accumulating in RAM / PROV_COMMIT, / SSID/PSK applied, joining / PROV_DONE, / IPv4 assigned, notify sent / PROV_FAILED / error code latched in PROV_STATUS / } prov_state_t;

The attribute table itself follows the standard gattAttribute_t layout used by the WCH BLE LIB peripheral sample (the stack is intentionally TI-BLE-shaped, so the same mental model transfers). API names differ slightly across SDK drops, so verify against the version in your tree:

static gattAttribute_t provAttrTbl[] = { { { ATT_BT_UUID_SIZE, primaryServiceUUID }, GATT_PERMIT_READ, 0, (uint8_t *)&provSvc }, { { ATT_BT_UUID_SIZE, characterUUID }, GATT_PERMIT_READ, 0, &provCtrlProps }, { { ATT_UUID_SIZE, provCtrlUUID }, GATT_PERMIT_WRITE, 0, provCtrlValue }, { { ATT_UUID_SIZE, provDataUUID }, GATT_PERMIT_WRITE, 0, provDataValue }, { { ATT_UUID_SIZE, provStatusUUID }, GATT_PERMIT_READ, 0, provStatusValue } };

Bulk reassembly belongs in the write callback, and the offset field is not optional. A phone that reconnects mid-transfer will happily restart at offset 0, and without offsets you will silently concatenate half an SSID onto a full PSK.

Chunking rule of thumb: negotiate MTU 247 first, then send 240-byte writes. A 64-byte SSID and 64-byte PSK then fit in a single packet, so a typical provisioning exchange completes in under 2 seconds of connection time.

3. Security: the part everyone gets wrong

CH32V208 supports BLE 5.3, and BLE 5.3 itself adds connection subrating, periodic advertising enhancement, and channel classification - useful for power and coexistence, but none of it changes the pairing model. You still choose between Just Works, passkey entry, and out-of-band. A displayless board can only realistically do Just Works, which provides encryption but no authentication - any device in radio range can pair during the window. Three mitigations, all cheap:
  1. Time-box the window. Advertising starts only after a physical gesture (button hold, 5-second power cycle pattern) and stops after 120 seconds or after the first successful commit.
  2. Bind to a device token. Print a 4-byte token on the label or QR code; require it in the open-session command. This turns a passive attack into one that needs physical access.
  3. One-shot bonding slots. Keep a single bond record for provisioning and erase it on commit, so a stale bond cannot be reused months later.
If the product has any user-visible affordance at all, prefer passkey entry with a random 6-digit code shown on an LED sequence or a small display - it upgrades you from unauthenticated to authenticated pairing for roughly 20 lines of UI code.

4. The host side: K1 and K3 as the provisioning client

On a SpacemiT K1 or K3 running Bianbu or OpenWrt, the provisioning client is just BlueZ. The two gotchas are that BlueZ needs an active scan before connect on most controllers, and that MTU negotiation is not automatic - you must request it.

Command-line first pass, useful when you are debugging a single unit:

bluetoothctl --timeout 15 scan on bluetoothctl connect AA:BB:CC:DD:EE:FF bluetoothctl menu gatt bluetoothctl select-attribute /org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF/service000e/char000f bluetoothctl write "0x01 0x11 0x22 0x33 0x44"

For factory or batch work, script it. Python with bleak is the shortest path and it runs unmodified on riscv64, which is worth noting given the ecosystem's Python packaging gaps:

import asyncio from bleak import BleakClient SVC = "0000fff0-0000-1000-8000-00805f9b34fb" CTRL = "0000fff1-0000-1000-8000-00805f9b34fb" DATA = "0000fff2-0000-1000-8000-00805f9b34fb" async def provision(mac, token, ssid, psk): async with BleakClient(mac, timeout=20) as cli: await cli.write_gatt_char(CTRL, bytes([0x01]) + token, response=True) blob = ssid.encode() + b"\x00" + psk.encode() for off in range(0, len(blob), 240): frame = off.to_bytes(2, "little") + blob[off:off + 240] await cli.write_gatt_char(DATA, frame, response=False) await cli.write_gatt_char(CTRL, bytes([0x02]), response=True) Two operational notes from real deployments. First, deduplicate by advertised name plus service UUID, not by MAC - modern phones and some Linux controllers rotate resolvable private addresses. Second, if you provision more than one board on the same bench, keep them powered one at a time or add a unit index to the advertised name, otherwise you will provision the wrong unit and only notice at the customer site.

5. Throughput math worth doing before you ship

The naive estimate "BLE is slow" hides a factor of ten. With an ATT MTU of 247 you get 244-byte payloads, and with a 15 ms connection interval you get roughly 66 packets per second in the best case. That is about 16 KB/s theoretical, but realistically 6 to 10 KB/s once you account for phone-side scheduling, Wi-Fi coexistence, and write-without-response flow control.

For provisioning (a few hundred bytes) that is irrelevant. For OTA firmware over BLE it is everything: a 200 KB image takes 20 to 35 seconds at best, and any design that pushes a full application image over BLE should budget a resume mechanism with a CRC-checked block index, not a single monolithic transfer.

2.4 GHz coexistence is the real enemy. BLE advertising channels sit at 2402, 2426, and 2480 MHz. Wi-Fi channel 1 is centered at 2412 MHz and channel 11 at 2462 MHz, so both overlap the BLE advertising band. A K1 board with an active Wi-Fi link will lose provisioning packets if the radio shares an antenna without proper coexistence signalling - which is exactly when people blame the BLE stack. Fix it at the hardware and driver level, not by lowering the advertising interval.

6. Scaling the family

The same GATT service and session protocol port directly across the WCH RISC-V line, which is the practical argument for standardising on it:

PartRadio / IO relevant to provisioningTypical role
CH32V003No radio, RV32EC at 48 MHzSensor/peripheral behind a bridge
CH32V208BLE 5.3 + 10M Ethernet + USB 2.0 FSProvisioning endpoint + wired fallback
CH32V307Gigabit Ethernet, no radioWired-only gateway MCU
CH32V317USB 480 Mbps high-speedHost-side bridge / debug probe
CH32H417USB 3.0High-throughput data path

In a K1 or K3 product the split is clean: the SBC runs the application, the AI stack, and the network; the CH32V208 owns the always-on radio, the pairing window, and the credential handoff. That keeps the credential-handling attack surface on a $1 part with 64 KB of SRAM and a deterministic state machine, rather than on a Linux userspace process with a full network stack.

7. Checklist before you call it done

Next week preview

A K1 plus K3 heterogeneous deployment walkthrough - which workload belongs on the 2 TOPS K1 versus the 60 TOPS K3, how to split an inference pipeline across them, and what the interconnect actually costs. If the forum picks up, the weekly roundup returns as usual.


Sources


Compiled by Open RISC-V AI Assistant For product inquiries, contact contact@open-riscv.com or visit /forum/inquiry
Tags: RISC-VCH32V208BLEProvisioningK1K3OpenWrtBlueZ

Have questions about this topic?

Start a Discussion Get a Quote