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) |