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

on_open

js320_fwup_on_open

on_close

js320_fwup_on_close

handle_cmd

js320_fwup_handle_cmd

handle_publish

js320_fwup_handle_publish

on_timeout

js320_fwup_on_timeout

finalize

js320_fwup_free

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

h/fwup/ctrl/!cmd

App → Driver

fwup_ctrl_cmd_s

h/fwup/ctrl/!rsp

Driver → App

fwup_rsp_s

h/fwup/fpga/!cmd

App → Driver

fwup_fpga_cmd_s

h/fwup/fpga/!rsp

Driver → App

fwup_rsp_s

Internal device-level topics (not used by applications):

Topic

Direction

Protocol

c/fwup/!cmd

Driver → Device

fw_cmd_s (16 bytes)

c/fwup/!rsp

Device → Driver

fw_cmd_s (16 bytes)

c/jtag/!cmd

Driver → Device

jtag_mem_s (12 bytes)

c/jtag/!rsp

Device → Driver

jtag_mem_s (12 bytes)

c/mode

Driver → Device

uint32 (FPGA only)

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 FW_CMD_OP_ERASE for slot 0 (workaround)

ALLOCATE

Send FW_CMD_OP_ALLOCATE with image size

WRITE

Pipelined 256-byte page writes

VERIFY

Pipelined 256-byte page reads + compare

UPDATE

Send FW_CMD_OP_UPDATE targeting slot 1

UPDATE Process Detail

  1. ERASE_PRE: The driver sends FW_CMD_OP_ERASE for slot 0 to clear the internal flash. If this fails, the operation aborts with JSDRV_ERROR_IO and slot 1 is not programmed.

  2. ALLOCATE: The driver sends a single FW_CMD_OP_ALLOCATE command to the device with the total image size. The device prepares a staging buffer and responds.

  3. WRITE: Pages are written using a sliding window pipeline. The driver sends up to pipeline_depth concurrent write commands. Each page is 256 bytes. The last page is padded with 0xFF to a full page boundary. As each response arrives, recv_idx advances and a new write may be sent if the window has room.

  4. 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 0xFF padding on the last page). A mismatch sets JSDRV_ERROR_IO.

  5. UPDATE: After successful verification, the driver sends FW_CMD_OP_UPDATE targeting 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 c/mode=2 (JTAG mode), wait 100ms

OPEN

Send JTAG_MEM_OP_OPEN

ERASE

Send JTAG_MEM_OP_ERASE_64K per block

WRITE

Pipelined 256-byte page writes

VERIFY

Pipelined 256-byte page reads + compare

CLOSE

Send JTAG_MEM_OP_CLOSE

MODE_RESTORE

Publish c/mode=0 (normal mode), wait 100ms

PROGRAM Process Detail

  1. MODE_SWITCH: The device must be in JTAG mode (mode 2) for SPI flash access. The driver publishes c/mode with value 2 and waits 100ms for the mode switch to take effect.

  2. OPEN: Sends JTAG_MEM_OP_OPEN to enter SPI background access mode via the JTAG interface.

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

  4. WRITE: Same sliding window pipeline as ctrl. Pages are 256 bytes, last page padded with 0xFF.

  5. VERIFY: Same pipelined read-and-compare as ctrl. Read data is compared byte-for-byte against the expected page content.

  6. CLOSE: Sends JTAG_MEM_OP_CLOSE to exit SPI background mode.

  7. MODE_RESTORE: Publishes c/mode with 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 send

  • recv_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

include_private/jsdrv_prv/js320_fwup.h

Public header

src/js320_fwup.c

State machines

src/js320_drv.c

Driver integration

example/minibitty/firmware.c

CLI example (ctrl)

example/minibitty/fpga_mem.c

CLI example (FPGA)