The RockBLOCK 9704 on Zephyr, and the pin wired upside down

Ground Control’s Iridium Certus modem running on Zephyr, with their C library unchanged, and the active-low pin I wired backwards.

A microcontroller on my back step sent 16 bytes through an Iridium satellite. The message was “Stamp-C3 says hi”, and it was in Cloudloop about ten seconds after the shell command that sent it. The Zephyr module that got it there is now public: rockblock9704-zephyr, MIT.

Why this modem

The modem is Ground Control’s RockBLOCK 9704, the Iridium Certus one. I have put about 70 of its older cousins, the 9602 and 9603, into Antarctic wave buoys since 2012. Those carry 340 bytes a message, so a buoy has to reduce a whole wave record to statistics on board before anything leaves. That has worked, and the science has always run on the statistics.

The 9704 carries messages of about 100 kB. With that much room the 2 Hz motion record itself can come home when a scientist on shore asks for it. The next buoy and the OGLAS satellite hub both use it.

There is no AT mode on the 9704. It speaks JSPR, JSON lines over a 230400 baud UART, with messages going out in segments, and Ground Control published the C library that speaks it under MIT.

Ground Control’s library

The module does not reimplement the protocol. west pins Ground Control’s library at a commit and the build compiles it as is, no patches. The module is the part Zephyr needs around it.

  • A serial port on whatever UART the devicetree names, with the modem as a node on it and its three control lines as optional GPIOs.
  • One worker thread that owns every call into the library. The library has no locking and keeps its state in static buffers, so I put all of it on one thread. The thread sleeps until a request or a byte from the modem arrives, so an idle modem costs nothing.
  • An API where every request carries a timeout. The library’s own send has none, and its async path only reacts to lines from the modem, so a modem that goes quiet mid-send leaves a message in flight for good. The module adds a cancel for that.
  • A shell command, so rb9704 info, rb9704 signal and rb9704 send work from any Zephyr console, and a spy that prints every JSPR line both ways.
  • A fake modem in Python, on a pty or a serial port. The same tests run against Ground Control’s own Python binding, against Zephyr on native_sim, and against Zephyr on a board through a two-board rig.

The sample runs on an STM32U575, an ESP32-S3 and an ESP32-C3, which is Arm, Xtensa and RISC-V, with nothing in the module tied to a CPU. That is the Zephyr argument from August paid back on a smaller scale: the modem is a few lines in a board overlay, and the same code builds for all three.

Reading the library before trusting it

Before writing the fake modem I went through the library’s source to find out what it sends and what it expects back, and kept a list as I went. It runs to 21 items. A few that a caller has to know:

  • The library asks for provisioning once and caches the answer, even when the answer was “not provisioned”. A modem provisioned after its first send refuses every topic until a reboot. The module clears the cache before a send whose topic it has not seen, and after a refusal.
  • The blocking calls stop reporting success once a callback is registered. The module uses the async path only.
  • A query during a send swallows the modem’s segment requests, because the reader discards lines it is not waiting for. Info and signal are refused with -EBUSY while a message is moving.
  • A line with no status code reads past the library’s buffer.

The module works around the ones it can. The rest are written up in the repository’s docs/JSPR.md for upstream, with line numbers.

What the hardware taught me

None of these three are in the source, and I found them in the wrong order.

USB-C shares the UART

The 9704’s USB-C goes through an FTDI chip to the modem’s serial lines, on the same pins as the header. With the cable plugged in, the FTDI drives the modem’s RX, so a board on the header can hear the modem but the modem never hears the board. Every begin failed with -EIO. It did not occur to me for a while that the cable was the problem; once it came out and the modem was powered from the header, begin worked.

Boot output before begin

On power-up the modem sends its boot lines whether or not anything has opened the library. On the Stamp-C3 those overflowed a 128-byte UART FIFO, the next begin heard fragments of old lines, and the one after that crashed the board inside the library’s line parser, which is the last item in the list above. The module now reads the UART from boot and discards what it was not open for, and when it opens part-way through a line it discards the rest of that line too.

P_EN

The third was a mistake of my own. Outside, the modem answered for two to five minutes after each begin, then went silent: every request -EIO, the signal reports stopped, nothing until the control lines were cycled. I did this four times before I went back to the header pinout.

P_EN is the supercapacitor charge enable, and Ground Control’s hardware page says so, in capitals: active low. My overlay said GPIO_ACTIVE_HIGH, so I had either not read that line or not believed it. Every begin drove the pin high, switched the charger off, and the modem ran on whatever the supercapacitor held until it was flat.

Pulling the wire off the header proved it: the same firmware stayed up and sent its first message. I fixed the overlay, put the wire back, and the second message went through. The modem then ran through the night, 11 hours 54 minutes without a drop and satellites visible throughout.

The module now also releases P_EN while the modem is off, so the board’s own pull-down keeps the charger on, and it follows Ground Control’s shutdown sequence: I_EN low, then wait for the booted pin to fall before touching anything else, which their page says matters.

Not done yet

I have not yet had a message come down to the board through a real satellite. The receive path is there and runs against the fake modem on the host and on the boards, single messages and two-segment ones. The next step is to do that under open sky.

Where this fits

This is the kind of work I do: Zephyr firmware for hardware that has to keep running where nobody can visit, satellite and LoRa links included. If you have a 9704 on the desk and a board to put it on, the module is MIT, and I am happy to talk.