JS320 Firmware Update Subsystem (js320_fwup)
Overview
The js320_fwup subsystem provides host-side firmware update and
FPGA bitstream programming for the JS320 device. It is implemented
as a driver subsystem within js320_drv, receiving a complete
image via pubsub and internally driving the page-level device
protocol. The caller publishes a single command with the full
image and receives a single completion response.
Motivation
Previously, firmware update and FPGA programming were implemented
as standalone example applications (example/minibitty/firmware.c
and example/minibitty/fpga_mem.c) that directly managed the
page-level device protocol using Windows-specific threading
primitives. Moving this logic into the driver:
Enables any language binding (Python, Node.js) to perform updates with a single publish-and-wait call.
Eliminates OS-specific threading code from callers.
Centralizes the protocol implementation, reducing duplication.
Architecture
Component Diagram
Application (C, Python, ...)
|
| h/fwup/ctrl/!cmd or h/fwup/fpga/!cmd
v
+-----------+
| js320_drv | (delegates via handle_cmd / handle_publish)
+-----------+
|
+-------------+
| js320_fwup | (state machines, pipelining, verification)
+-------------+
|
| c/fwup/!cmd (ctrl) or c/jtag/!cmd (fpga)
v
JS320 Device
Subsystem Delegation
js320_drv.c owns the js320_fwup_s instance and delegates
to it via the standard driver callback pattern:
Driver callback |
fwup function |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Each handle_cmd and handle_publish callback returns true
if the topic was consumed, allowing the driver to chain multiple
subsystems (js320_fwup is checked before js320_jtag).
Threading Model
The fwup subsystem runs entirely on the driver’s backend thread.
No OS synchronization primitives are needed internally. Pipelining
uses an event-driven sliding window tracked by send_idx and
recv_idx counters. Each device response advances recv_idx
and may trigger the next send.
Large Payload Handling
Firmware images can be several megabytes. The pubsub message
system handles this automatically via jsdrvp_msg_alloc_value(),
which heap-allocates payloads exceeding JSDRV_PAYLOAD_LENGTH_MAX
(1024 bytes) and sets the JSDRV_UNION_FLAG_HEAP_MEMORY flag.
The fwup subsystem copies the image data into its own buffer
on command receipt, so the message is safely freed afterward.
Topics
Topic |
Direction |
Payload |
|---|---|---|
|
App → Driver |
|
|
Driver → App |
|
|
App → Driver |
|
|
Driver → App |
|
Internal device-level topics (not used by applications):
Topic |
Direction |
Protocol |
|---|---|---|
|
Driver → Device |
|
|
Device → Driver |
|
|
Driver → Device |
|
|
Device → Driver |
|
|
Driver → Device |
|
Message Formats
All structures are packed with natural alignment (no padding).
Ctrl Command (fwup_ctrl_cmd_s, 8 bytes + data)
Offset Size Field
0 4 transaction_id Caller-assigned ID, echoed in response
4 1 op fwup_ctrl_op_e (UPDATE=1, LAUNCH=2, ERASE=3)
5 1 image_slot Target image slot (0-1)
6 1 pipeline_depth 0=default(8), max 16
7 1 rsv Reserved, set to 0
8 N data[] Image bytes (UPDATE only)
FPGA Command (fwup_fpga_cmd_s, 8 bytes + data)
Offset Size Field
0 4 transaction_id Caller-assigned ID, echoed in response
4 1 op fwup_fpga_op_e (PROGRAM=1)
5 1 pipeline_depth 0=default(8), max 16
6 2 rsv Reserved, set to 0
8 N data[] FPGA bitstream bytes
Response (fwup_rsp_s, 8 bytes)
Offset Size Field
0 4 transaction_id Echoed from command
4 4 status 0=success, JSDRV_ERROR_* on failure
Ctrl Firmware Update
The ctrl path manages the JS320 microcontroller firmware stored in flash image slots. It supports three operations.
Operations
UPDATE (FWUP_CTRL_OP_UPDATE = 1):
Write a firmware image to the device staging buffer, verify
it, and commit to the specified image slot.
LAUNCH (FWUP_CTRL_OP_LAUNCH = 2):
Boot the device from the specified image slot. The device
resets after accepting this command.
ERASE (FWUP_CTRL_OP_ERASE = 3):
Erase the specified image slot.
Locked-mode Workaround
The JS320 in locked mode cannot program the internal flash
(image slot 0). The UPDATE flow erases slot 0 and programs the
external SPI flash (image slot 1) instead. The caller’s
image_slot field is currently ignored for UPDATE.
UPDATE State Machine
IDLE ──cmd──> ERASE_PRE ──rsp──> ALLOCATE ──rsp──> WRITE ──all done──>
VERIFY ──all done──> UPDATE ──rsp──> respond to app, return to IDLE
State |
Action |
|---|---|
ERASE_PRE |
Send |
ALLOCATE |
Send |
WRITE |
Pipelined 256-byte page writes |
VERIFY |
Pipelined 256-byte page reads + compare |
UPDATE |
Send |
UPDATE Process Detail
ERASE_PRE: The driver sends
FW_CMD_OP_ERASEfor slot 0 to clear the internal flash. If this fails, the operation aborts withJSDRV_ERROR_IOand slot 1 is not programmed.ALLOCATE: The driver sends a single
FW_CMD_OP_ALLOCATEcommand to the device with the total image size. The device prepares a staging buffer and responds.WRITE: Pages are written using a sliding window pipeline. The driver sends up to
pipeline_depthconcurrent write commands. Each page is 256 bytes. The last page is padded with0xFFto a full page boundary. As each response arrives,recv_idxadvances and a new write may be sent if the window has room.VERIFY: Pages are read back and compared against the original image using the same pipelining strategy. Each response carries 256 bytes of read data which is compared byte-for-byte against the expected page content (including
0xFFpadding on the last page). A mismatch setsJSDRV_ERROR_IO.UPDATE: After successful verification, the driver sends
FW_CMD_OP_UPDATEtargeting slot 1 (external SPI flash). The device commits the staging buffer to flash and responds. The driver then sends the completion response to the application.
LAUNCH and ERASE
These are single-command operations. The driver sends the
corresponding FW_CMD_OP_LAUNCH or FW_CMD_OP_ERASE to the
device and forwards the response to the application.
Device Protocol (c/fwup)
The ctrl path uses the fw_cmd_s device protocol (16-byte
header, optionally followed by page data):
Offset Size Field
0 4 id Sequence ID
4 1 op fw_cmd_op_e
5 1 status 0=ok (responses only)
6 1 image Image slot
7 1 rsv
8 4 offset Byte offset within staging buffer
12 4 length Data length
16 N data[] Page data (write cmd, read rsp)
FPGA Programming
The FPGA path programs the ECP5 SPI configuration flash via the JTAG interface. It manages the entire lifecycle: mode switch, JTAG open, flash erase, pipelined write, pipelined verify, JTAG close, and mode restore.
PROGRAM State Machine
IDLE ──cmd──> MODE_SWITCH ──timeout 100ms──> OPEN ──rsp──>
ERASE ──all blocks──> WRITE ──all pages──> VERIFY ──all pages──>
CLOSE ──rsp──> MODE_RESTORE ──timeout 100ms──> respond to app,
return to IDLE
State |
Action |
|---|---|
MODE_SWITCH |
Publish |
OPEN |
Send |
ERASE |
Send |
WRITE |
Pipelined 256-byte page writes |
VERIFY |
Pipelined 256-byte page reads + compare |
CLOSE |
Send |
MODE_RESTORE |
Publish |
PROGRAM Process Detail
MODE_SWITCH: The device must be in JTAG mode (mode 2) for SPI flash access. The driver publishes
c/modewith value 2 and waits 100ms for the mode switch to take effect.OPEN: Sends
JTAG_MEM_OP_OPENto enter SPI background access mode via the JTAG interface.ERASE: Erases 64KB blocks covering the image size. Blocks are erased sequentially (one at a time) since flash serializes erase operations internally. Block count =
ceil(image_size / 65536).WRITE: Same sliding window pipeline as ctrl. Pages are 256 bytes, last page padded with
0xFF.VERIFY: Same pipelined read-and-compare as ctrl. Read data is compared byte-for-byte against the expected page content.
CLOSE: Sends
JTAG_MEM_OP_CLOSEto exit SPI background mode.MODE_RESTORE: Publishes
c/modewith value 0 to return the device to normal operation mode and waits 100ms.
Error Cleanup
If an error occurs during OPEN, ERASE, WRITE, or VERIFY, the state machine transitions through CLOSE and MODE_RESTORE before responding to the application. This ensures the JTAG interface is properly closed and the device mode is restored regardless of where the failure occurred.
Device Protocol (c/jtag)
The FPGA path uses the jtag_mem_s device protocol (12-byte
header, optionally followed by page data):
Offset Size Field
0 4 transaction_id
4 1 operation jtag_mem_op_e
5 1 status 0=ok (responses only)
6 2 timeout_ms
8 4 offset Flash byte offset
12 N data[] Page data (write cmd, read rsp)
Pipelining
Both ctrl and FPGA paths use the same pipelining strategy for write and verify (read) operations. This significantly improves throughput by overlapping USB round-trips.
Sliding Window
The pipeline is a fixed-size sliding window controlled by two counters:
send_idx: next page index to sendrecv_idx: next page index expected in a response
The window size is send_idx - recv_idx, bounded by
pipeline_depth (default 8, max 16).
Event Flow
1. Enter WRITE/VERIFY: send_idx=0, recv_idx=0
2. Send up to pipeline_depth commands (send_idx advances)
3. On each response:
a. recv_idx++
b. If error: wait for outstanding responses, then fail
c. If recv_idx == page_count: transition to next state
d. Else if window has room: send next command
Error Handling
On a device error response, the error is recorded and no new
commands are sent. The driver continues to receive responses for
already-sent commands (draining the pipeline). Once
recv_idx >= send_idx (all outstanding responses received),
the operation fails with JSDRV_ERROR_IO.
Usage
C Example (ctrl firmware update)
// Open device and subscribe to response
jsdrv_open(ctx, device, JSDRV_DEVICE_OPEN_MODE_RESUME, timeout);
jsdrv_subscribe(ctx, "{device}/h/fwup/ctrl/!rsp",
JSDRV_SFLAG_PUB, on_rsp, NULL, 0);
// Build command: 8-byte header + image
uint32_t cmd_size = 8 + image_size;
uint8_t * buf = malloc(cmd_size);
struct fwup_ctrl_cmd_s * cmd = (void *) buf;
cmd->transaction_id = 1;
cmd->op = FWUP_CTRL_OP_UPDATE; // 1
cmd->image_slot = 0;
cmd->pipeline_depth = 8;
cmd->rsv = 0;
memcpy(cmd->data, image, image_size);
jsdrv_publish(ctx, "{device}/h/fwup/ctrl/!cmd",
&jsdrv_union_bin(buf, cmd_size), 0);
free(buf);
// Wait for on_rsp callback with fwup_rsp_s
Python Example (ctrl firmware update)
d = Driver()
d.open(device, 'restore')
image = open('firmware.mbfw', 'rb').read()
cmd = struct.pack('<IBBBB', 1, 1, 0, 8, 0) + image
rsp = d.publish_and_wait(
f'{device}/h/fwup/ctrl/!cmd', cmd,
f'{device}/h/fwup/ctrl/!rsp', timeout=60.0)
status = struct.unpack('<Ii', rsp[:8])
Python Example (FPGA programming)
bitstream = open('design.bit', 'rb').read()
cmd = struct.pack('<IBBH', 1, 1, 8, 0) + bitstream
rsp = d.publish_and_wait(
f'{device}/h/fwup/fpga/!cmd', cmd,
f'{device}/h/fwup/fpga/!rsp', timeout=120.0)
status = struct.unpack('<Ii', rsp[:8])
Files
File |
Description |
|---|---|
|
Public header |
|
State machines |
|
Driver integration |
|
CLI example (ctrl) |
|
CLI example (FPGA) |