Version 2.0 — May 2026 SpaceCommsKit — https://spacecommskit.com/docs
This document covers the full SCK-915 development stack — from the OpenLST protocol foundation through the production SCK-PBL-1 hardware. If you are new to the system, read Section 1 before anything else.
Every cross-reference in the firmware source uses a [SCK-DEV: TAG] comment.
Search for any tag in the codebase to jump directly to the relevant section:
| Tag | Topic | Section |
|---|---|---|
SCK-DEV: BOARD_CONFIG |
Hardware configuration overview | 2.1 |
SCK-DEV: RF_FRONTEND |
CC1190 PA/LNA control | 2.2 |
SCK-DEV: RF_CONFIG |
915MHz register values | 2.3 |
SCK-DEV: TX_POWER |
Power mode / bench vs field | 2.4 |
SCK-DEV: BOOTLOADER |
Bootloader timeout | 2.5 |
SCK-DEV: ADD_COMMAND |
Adding new commands | 3.1 |
SCK-DEV: CHUNKING |
Chunking large responses | 3.2 |
SCK-DEV: TIMING |
Prototype board timing issues | 3.3 |
SCK-DEV: ESP_FRAMING |
UART framing protocol | 3.4 |
SCK-DEV: RESPONSE_FORMAT |
Response string conventions | 3.5 |
SCK-DEV: BEACON |
Autonomous GPS beacon | 3.6 |
SCK-DEV: SD_FLIGHT_LOG |
SD card flight log format | 3.7 |
Ground Station (Windows C#)
↕ USB / Serial — COM port — 115200 baud — ESP framing
SCK-915 RF Board (CC1110 + CC1190) ← ground unit
↕ 915MHz RF — 7.4 kBaud — 2-FSK + FEC
SCK-915 RF Board (CC1110 + CC1190) ← remote / payload unit
↕ UART0 — 115200 baud — ESP framing
SCK-PBL-1 Payload Board (Raspberry Pi Pico)
├── OV2640 Camera (SPI0 + I2C0)
├── MicroSD Card (SPI0)
├── GPS NEO-6M (UART1)
└── BMP581 Altimeter (I2C1)
The ground unit RF board relays packets transparently between the ground
station and the remote unit. Only the remote unit requires the custom
PICO_MSG handler in board.c. This mirrors the original OpenLST
Explorer Kit architecture where Board 0001 was the transparent relay
and Board 0004 ran the custom firmware.
| Board | Description | Status |
|---|---|---|
| SCK-915 Prototype | Hand-wired jumper development board | Dev/test only — see Section 3.3 |
| SCK-915 RF Board | CC1110 + CC1190 RF front end PCB | Production |
| SCK-PBL-1 v1.0 Payload | Pico + camera + GPS + BMP581 + SD PCB | Production — BMP581 altimeter |
| SCK-PBL-1 v1.1 Payload | Pico + camera + GPS + MS5611 + SD PCB | Production — MS5611 altimeter |
| File | Language | Runs On | Purpose |
|---|---|---|---|
board.h |
C | CC1110 | Hardware configuration and defines |
board.c |
C | CC1110 | Board init and PICO_MSG relay |
main.py |
MicroPython | Pico | Payload pipeline and command handlers (MS5611 active, BMP581 preserved as comments) |
sdcard.py |
MicroPython | Pico | MicroPython SD card SPI driver |
openlst-master/
├── Makefile ← top-level build
├── open-lst/ ← DO NOT MODIFY
│ ├── common/ ← uart0.c uart1.c radio.c ...
│ ├── radio/ ← main.c commands.c ...
│ └── bootloader/
└── radios/openlst_437/ ← YOUR CODE LIVES HERE
├── Board.mk ← build config
├── board.h ← hardware defines + custom_commands declaration
└── board.c ← board_init() + custom_commands()
Never modify files inside
open-lst/. All customization goes inradios/openlst_437/. This keeps your changes clean and upstream updates safe.
board.h [SCK-DEV: BOARD_CONFIG]board.h configures the OpenLST firmware for SCK-915 hardware.
Key defines:
| Define | Value | Purpose |
|---|---|---|
F_CLK |
27000000 | Crystal frequency — must match physical crystal |
CUSTOM_BOARD_INIT |
1 | Enables board_init() in board.c |
BOARD_HAS_TX_HOOK |
1 | Enables board_pre_tx() macro |
BOARD_HAS_RX_HOOK |
1 | Enables board_pre_rx() macro |
ADCCFG_CONFIG |
0b00000011 | Enables AN0 and AN1 for voltage sense |
RADIO_RANGING_RESPONDER |
1 | Enables ranging response |
CUSTOM_COMMANDS |
1 | Enables custom command dispatch |
Build flags (set in Board.mk):
openlst_437_CFLAGS := \
-DCUSTOM_BOARD_INIT \
-DCUSTOM_COMMANDS \
-DUART0_ENABLED=1 \
-I$(openlst_437_DIR)
Always use Clean + Build after any source change. Stale
.relobject files are the most common cause of mysterious firmware behavior that is difficult to trace. This is not optional — it is required.
The CC1190 provides PA and LNA functions between the CC1110 and antenna.
Physical connections:
| CC1190 Pin | Signal | CC1110 Pin | Control Method |
|---|---|---|---|
| Pin 8 (PA_EN) | PA enable | P1.6 (GDO1) | Automatic via IOCFG1 |
| Pin 7 (LNA_EN) | LNA enable | P1.7 (GDO2) | Automatic via IOCFG2 |
| Pin 6 (HGM) | High gain mode | P2.0 | Software via _rf_high_gain |
HGM power modes:
_rf_high_gain |
HGM State | Total Output Power | Use Case |
|---|---|---|---|
| 0 | LOW | ~+10 dBm | Bench / indoor testing |
| 1 | HIGH | ~+27 dBm | Field / HAB / LEO mission |
GDO pins are configured using inverted power-down signals so the CC1190 enable lines go HIGH automatically when the corresponding stage is active. The firmware never manually toggles PA_EN or LNA_EN — the CC1110 radio hardware manages this through the IOCFG register settings.
All register values were derived using SmartRF Studio 7 for the CC1110 with a 27MHz crystal.
| Parameter | Value |
|---|---|
| Base Frequency | 914.999512 MHz |
| Crystal | 27.000 MHz |
| Modulation | 2-FSK |
| Data Rate | 7.415 kBaud |
| Deviation | 3.708 kHz |
| RX Filter BW | 60.268 kHz |
Frequency control word calculation:
FREQ[23:0] = (Fcarrier / Fxtal) × 65536
= (914999512 / 27000000) × 65536
= 0x21E38D
So RF_FREQ2 = 0x21, RF_FREQ1 = 0xE3, RF_FREQ0 = 0x8D.
To change frequency:
RF_FREQ2, RF_FREQ1, RF_FREQ0 in board.hRF_FSCTRL1 if moving to a different frequency bandPower mode is set at flash time — not at runtime.
RF_PA_CONFIG |
CC1110 Output | Total w/CC1190 | Firmware Build |
|---|---|---|---|
0xC0 |
~0 dBm | ~18 dBm | Bench / indoor (default) |
0xC2 |
~10 dBm | ~28 dBm | Field / HAB / LEO |
The ground station patches RF_PA_CONFIG automatically when flashing
bench or field firmware.
Critical: There must be exactly one
#define RF_PA_CONFIGline inboard.h. The ground station patching tool searches for this exact string. Adding duplicates or moving this line will break the patching tool silently.
COMMAND_WATCHDOG_DELAY = 1350000 ≈ 600ms at 27MHz.
The OpenLST default (45000 ≈ 20ms) is too short for RF OTA flashing. The 600ms window gives the ground station time to detect the bootloader beacon and transmit a flash command before the bootloader times out and jumps to the application. Do not reduce below 1,000,000 for OTA-capable deployments.
After a direct serial flash, warm up the board with 2–3 get_telemetry
commands before attempting OTA operations. This allows the RF synthesizer
to stabilize fully.
Adding a new command requires changes in three places. The CC1110 is a
transparent relay for all PICO_MSG (0x20) commands — it forwards any
payload to the Pico regardless of sub-opcode, so board.c typically
requires no changes.
main.py# Add to the sub-opcode constants at the top of main.py
CMD_MY_COMMAND = 0x0A # Next available opcode after 0x09
main.py command dispatch loopelif sub_opcode == CMD_MY_COMMAND:
try:
# payload[0] = sub_opcode
# payload[1:] = command-specific data
param = payload[1:].decode('utf-8').strip()
result = do_something(param)
# Must fit in MAX_PAYLOAD (251 bytes)
# For larger responses implement chunking — see Section 3.2
msg = f"MYRESP:{result}"
send_esp(msg.encode())
blink(1)
print(f"MY_COMMAND → {msg}")
except Exception as e:
send_esp(b"MYRESP:ERR:FAIL")
print(f"MY_COMMAND error: {e}")
// In CommandOpcode enum
MyCommand = 0x0A,
// Command method
private async Task MyCommandAsync() {
if (!CheckConnected()) return;
ushort hwid = ActiveHwid;
ushort seq = IncSeqNum();
FlushRxQueue();
WritePacket(OpenLstProtocol.BuildPacket(
hwid, seq, 0x20, new byte[] { 0x0A }));
var pkt = await WaitForReply(hwid, seq, 10000);
if (pkt?.PicoPayload != null)
Log($"Response: {pkt.PicoPayload}", Theme.Green);
else
Log("No response", Theme.Red);
}
Only modify board.c if your command needs a timeout longer than the
standard 800ms (e.g. commands involving SD card writes):
// Add timeout constant
#define PICO_TIMEOUT_OUTER_MY_SLOW_CMD 300 // ~3 seconds
// Add sub-opcode check in custom_commands()
outer_max = (payload_len > 0 && cmd->data[0] == PICO_SUB_MY_SLOW_CMD)
? PICO_TIMEOUT_OUTER_MY_SLOW_CMD
: PICO_TIMEOUT_OUTER;
These rules come from hard experience. Violating any of them produces difficult-to-trace failures:
| Rule | Consequence if violated |
|---|---|
Never set reply->header.hwid, seqnum, or system |
Memory corruption — RF stops working |
Only set reply->header.command |
It is the only field custom_commands owns |
Never use WATCHDOG_CLEAR macro |
Corrupts RF receive via register conflict |
Never declare static __xdata buffers |
XDATA RAM overflow — board crashes |
Use uint8_t/uint16_t only for counters |
uint32_t causes RAM overflow crash |
| Use nested loops for timeouts >65535 | uint16_t wraps silently — timeout too short |
| Always send ≥1 byte payload | Zero-length ESP frames are silently rejected |
The maximum RF packet payload is 251 bytes (MAX_PAYLOAD). Responses
larger than this must be split into chunks.
"LIST:file1.jpg,file2.jpg," ← first chunk — trailing comma
"LIST+:file3.jpg,file4.jpg," ← continuation — trailing comma
"LIST+:file5.jpg" ← final chunk — NO trailing comma = end signal
The ground station accumulates chunks until a packet with no trailing comma arrives, then reassembles.
Implementation template:
CHUNK = MAX_PAYLOAD - 8 # Reserve space for prefix
chunk_items = []
chunk_len = 0
first_chunk = True
for item in items:
entry = item + ","
if chunk_len + len(entry) > CHUNK and chunk_items:
prefix = "MYLIST:" if first_chunk else "MYLIST+:"
send_esp((prefix + ",".join(chunk_items) + ",").encode())
first_chunk = False
chunk_items = [item]
chunk_len = len(item) + 1
else:
chunk_items.append(item)
chunk_len += len(entry)
# Final chunk — no trailing comma signals end of list
if chunk_items:
prefix = "MYLIST:" if first_chunk else "MYLIST+:"
send_esp((prefix + ",".join(chunk_items)).encode())
Used for transferring binary files such as JPEG images and .sckflight logs.
Protocol sequence:
CMD_GET_INFO with filename"INFO:<filename>:<total_bytes>:<chunk_count>"CMD_GET_CHUNK for index 0, 1 … chunk_count-1"CHUNK:<index>:<200 bytes binary>" for eachChunk size: 200 bytes (CHUNK_SIZE) — leaves 51 bytes for the
"CHUNK:65535:" prefix within the 251-byte limit.
# GET_INFO handler
size = uos.stat(f"/sd/{filename}")[6]
chunks = (size + CHUNK_SIZE - 1) // CHUNK_SIZE
send_esp(f"INFO:{filename}:{size}:{chunks}".encode())
# GET_CHUNK handler
chunk_index = payload[1] | (payload[2] << 8) # little-endian uint16
filename = payload[3:].decode('utf-8').strip()
with open(f"/sd/{filename}", 'rb') as f:
f.seek(chunk_index * CHUNK_SIZE)
data = f.read(CHUNK_SIZE)
send_esp(f"CHUNK:{chunk_index}:".encode() + data)
This section applies to the hand-wired SCK-915 prototype board only. The SCK-PBL-1 production PCB resolves all of these issues through proper PCB routing and direct trace connections.
Jumper wire capacitance and crosstalk limit the SPI bus to 400kHz on the
prototype. The production target on SCK-PBL-1 is 1.32MHz (the sdcard.py
design speed).
Symptoms: SD card mount fails, camera produces corrupt images,
SD mount failed: in serial output.
SCK-PBL-1 fix: Change in main.py:
# Prototype (400kHz — safe for jumper wires)
spi = SPI(0, baudrate=400000, ...)
# SCK-PBL-1 production (1.32MHz — sdcard.py design speed)
spi = SPI(0, baudrate=1320000, ...)
# Also update slow init in mount_sd():
# Prototype: baudrate=100000
# SCK-PBL-1: baudrate=400000
Verify SD card reliability at 1.32MHz before deploying. Some cards support up to 4MHz over short PCB traces for faster file transfers.
Jumper wire noise can delay Pico UART responses enough to trigger the CC1110 timeout, causing intermittent NACKs even though the Pico completed the command correctly.
Symptoms: Commands NACK intermittently. Pico serial output shows command completed. Same command sometimes succeeds, sometimes fails.
Fix: Increase PICO_TIMEOUT_OUTER in board.c:
#define PICO_TIMEOUT_OUTER 40 // ~800ms standard
#define PICO_TIMEOUT_OUTER 80 // ~1600ms for prototype
Camera and SD card share SPI0. Both CS pins must be HIGH before switching between devices. Missing a CS deselect causes data corruption on both devices.
# Always do this before switching between camera and SD
cam_cs(1); sd_cs(1)
time.sleep_ms(10)
# Then select the device you need
Do not remove the explicit cs(1) calls — they are required on both
prototype and production boards.
Camera initialization over I2C takes several hundred milliseconds. Initializing at boot blocks UART0 and can cause the CC1110 to miss the first command after power-on.
Fix (already implemented): Camera init is deferred to the first idle
timeout in the main loop, not at boot. Do not move init_camera() back
to the startup sequence.
The Arducam OV2640 requires direct wire connections with minimal capacitance. Breadboard connections add enough capacitance to cause intermittent SPI read failures on the FIFO readout.
Fix: Use direct wired connections or the SCK-PBL-1 PCB traces. Do not use breadboard for the camera SPI connections.
All UART0 communication between CC1110 and Pico uses ESP framing:
[0x22][0x69][length][payload bytes...]
↑ ↑ ↑ ↑
Start Start 1 byte 1-251 bytes
byte0 byte1 payload command data
length
OpenLST packet structure inside the payload:
| Byte | Field | Notes |
|---|---|---|
| 0-1 | HWID | Little-endian destination hardware ID |
| 2-3 | Sequence | Little-endian seq number (16–64000) |
| 4 | System | 0x01 = MSG_TYPE_RADIO_IN (required) |
| 5 | Opcode | Command opcode byte |
| 6+ | Data | Command-specific payload |
Critical: Length must be ≥ 1. Zero-length ESP frames are silently rejected by the RX interrupt service routine.
MicroPython implementation:
ESP_START_0 = 0x22
ESP_START_1 = 0x69
MAX_PAYLOAD = 251
def send_esp(payload_bytes):
uart.write(bytes([ESP_START_0, ESP_START_1,
len(payload_bytes)]) + payload_bytes)
def recv_esp(timeout_ms=5000):
S0, S1, SL, SP = 0, 1, 2, 3
state, payload, remaining = S0, bytearray(), 0
deadline = time.ticks_add(time.ticks_ms(), timeout_ms)
while time.ticks_diff(deadline, time.ticks_ms()) > 0:
if uart.any():
b = uart.read(1)[0]
if state == S0:
if b == ESP_START_0: state = S1
elif state == S1:
if b == ESP_START_1: state = SL
elif b == ESP_START_0: state = S1
else: state = S0
elif state == SL:
if 1 <= b <= 251:
remaining, payload, state = b, bytearray(), SP
else: state = S0
elif state == SP:
payload.append(b); remaining -= 1
if remaining == 0: return bytes(payload)
else:
time.sleep_ms(1)
return None
All Pico responses follow this format:
PREFIX:field1,field2,...
Conventions:
PICO, SNAP, GPS, etc.)PREFIX:ERR:REASON format: and ,Complete response table:
| Command | Opcode | Success Response | Error Response |
|---|---|---|---|
| PING | 0x00 | PICO:ACK |
— |
| READ_TEMP | 0x01 | TEMP:23.45C |
— |
| SNAP | 0x02 | SNAP:OK:snap_001.jpg:24576 |
SNAP:ERR:NO_SD / SNAP:ERR:NO_CAM |
| LIST | 0x03 | LIST:file1.jpg,file2.jpg |
LIST:ERR:NO_SD / LIST:EMPTY |
| GET_INFO | 0x04 | INFO:snap_001.jpg:24576:123 |
INFO:ERR:NOTFOUND |
| GET_CHUNK | 0x05 | CHUNK:0:<200 bytes> |
CHUNK:ERR:FAIL |
| DELETE | 0x06 | DEL:OK:snap_001.jpg |
DEL:ERR:FAIL |
| GET_GPS | 0x07 | GPS:35.12,-97.56,1523,8,1,1013.25,1521.3,22.5 |
— |
| GET_BARO | 0x08 | BARO:1013.25,1521.3,22.5 |
BARO:ERR:NOT_READY |
| BEACON_CTRL | 0x09 | BEACON:ON / BEACON:OFF |
— |
The Pico transmits a fused GPS+barometric packet autonomously every 10 seconds when no command is being processed and the beacon is enabled.
Beacon interval: GPS_BEACON_INTERVAL_MS = 10000 (10 seconds)
Beacon format:
GPS:lat,lon,gps_alt,sats,fix,hpa,baro_alt,temp_c
Controlling the beacon:
CMD_BEACON_CTRL (0x09) + 0x01 → enable
CMD_BEACON_CTRL (0x09) + 0x00 → disable
Disable during high-rate file transfers to prevent beacon packets from interleaving with file chunk responses. Re-enable after the transfer.
Every beacon packet is also written to the SD flight log — see Section 3.7.
One .sckflight file is created per power cycle.
Naming: FLT-001.sckflight, FLT-002.sckflight, etc.
File format (JSON):
{"flight_id":"FLT-001","hardware":"SCK-915+SCK-PBL-1","packets":[
{"t":0.0,"lat":0.0,"lon":0.0,"gps_alt":0.0,"sats":0,"fix":0,
"baro_hpa":1013.25,"baro_alt":215.3,"baro_temp":22.5,"event":""},
{"t":10.1,"lat":35.123456,"lon":-97.654321,"gps_alt":1523.4,
"sats":8,"fix":1,"baro_hpa":849.12,"baro_alt":1521.3,
"baro_temp":18.2,"event":"SNAP:snap_001.jpg"}
],
"summary":{"total_packets":42,"flight_duration_s":420.0}}
Adding new fields:
In write_flight_packet() in main.py:
line = (f'{sep}{{"t":{t},'
f'"lat":{gps_fix["lat"]:.6f},'
# existing fields...
f'"my_new_field":{my_value},' # ← add here
f'"event":"{event}"}}\n')
Also update the ground station FlightReplayForm parser.
| GPIO | Function | Notes |
|---|---|---|
| 0 | UART0 TX → CC1110 | ESP framing to RF board |
| 1 | UART0 RX ← CC1110 | ESP framing from RF board |
| 2 | I2C1 SDA (MS5611) | 400kHz — same pins as previous BMP581 |
| 3 | I2C1 SCL (MS5611) | 400kHz |
| 4 | I2C0 SDA (Camera) | 100kHz |
| 5 | I2C0 SCL (Camera) | 100kHz |
| 8 | UART1 TX → GPS | 9600 baud — UBX config only |
| 9 | UART1 RX ← GPS | 9600 baud — NMEA stream in |
| 10 | LED Camera (GREEN) | Solid = camera initialized OK |
| 11 | LED SD Card (GREEN) | Solid = SD mounted OK |
| 12 | LED GPS (BLUE) | Slow flash = searching / Solid = fix |
| 13 | LED Altimeter (YELLOW) | Solid = BMP581 initialized OK |
| 14 | LED Fault (RED) | Solid = any subsystem failed |
| 15 | Camera CS | Active LOW — deselect before SD access |
| 16 | SD Card CS | Active LOW — deselect before camera access |
| 17 | SPI0 MOSI | Shared camera + SD |
| 18 | SPI0 SCLK | Shared camera + SD |
| 19 | SPI0 MISO | Shared camera + SD |
| 20–24 | Free GPIO | Available for expansion payloads |
| 25 | Onboard LED | Boot confirmation blinks |
| 26–28 | Free GPIO | Available for expansion payloads |
The SCK-PBL-1 payload board includes two 2×20 pin headers that break out all Raspberry Pi Pico GPIO pins — including pins already in use by the on-board subsystems. This allows researchers and developers to attach additional payload hardware without requiring a new PCB revision for each mission variant.
J_EXP1 and J_EXP2 — full Pico pin breakout:
J_DEBUG — dedicated debug header:
Free GPIOs for expansion: 20–24 and 26–28 (seven free GPIO pins)
When attaching expansion hardware:
| Method | Description |
|---|---|
ActiveHwid |
Property: current HWID from header bar |
IncSeqNum() |
Increment and return next sequence number |
FlushRxQueue() |
Discard queued received packets before sending |
WritePacket(byte[]) |
Send raw bytes to serial port |
WaitForReply(hwid,seq,ms) |
Async: wait for matching ack/response |
Log(message, color) |
Append line to main log panel |
LogTx(message, hwid) |
Log transmitted packet in yellow TX format |
CheckConnected() |
Returns false and logs error if not connected |
MkButton(text,x,y,w,fg,bg) |
Create styled flat button |
MkLabel(text,x,y,w,color,font) |
Create styled label |
MkSectionLabel(text,x,y) |
Create cyan section divider |
MkTab(text) |
Create styled tab page |
private async Task MyCommandAsync() {
if (!CheckConnected()) return;
ushort hwid = ActiveHwid;
ushort seq = IncSeqNum();
FlushRxQueue();
WritePacket(OpenLstProtocol.BuildPacket(
hwid, seq, 0x20, new byte[] { 0x0A })); // sub-opcode 0x0A
var pkt = await WaitForReply(hwid, seq, 10000);
if (pkt?.PicoPayload != null)
Log($"Response: {pkt.PicoPayload}", Theme.Green);
else
Log("No response", Theme.Red);
}
| Opcode | Name | Description |
|---|---|---|
| 0x10 | common_msg_ack |
Acknowledge response |
| 0xFF | common_msg_nack |
Not-acknowledge response |
| 0x11 | common_msg_ascii |
ASCII debug text |
| 0x12 | radio_msg_reboot |
Reboot (optional 4-byte postpone_sec) |
| 0x13 | radio_msg_get_time |
Request RTC time |
| 0x14 | radio_msg_set_time |
Set RTC time |
| 0x17 | radio_msg_get_telem |
Request telemetry |
| 0x18 | radio_msg_telem |
Telemetry response (83 bytes) |
| 0x19 | radio_msg_get_callsign |
Request callsign |
| 0x1A | radio_msg_set_callsign |
Set callsign (8 bytes) |
| 0x20 | PICO_MSG |
Forward payload to Pico via UART0 |
# Bench firmware (safe indoor power — default)
mingw32-make openlst_437_radio RF_PA_CONFIG=0xC0
# Field firmware (full mission power)
mingw32-make openlst_437_radio RF_PA_CONFIG=0xC2
Or use the ground station "Flash Firmware" button which handles build selection and patching automatically.
Always Clean + Build after any source change. Never trust an incremental build after modifying
board.horboard.c.
# Using mpremote
mpremote cp main.py :main.py
mpremote cp sdcard.py :sdcard.py
Or hold BOOTSEL on power-up and drag-and-drop files to the USB mass storage drive.
get_telemetry commands after OTA flash before other operationsThe 600ms window is controlled by COMMAND_WATCHDOG_DELAY in board.h.
These lessons were accumulated during development of the SCK-915 system starting from the OpenLST Explorer Kit prototype. Each one cost real debugging time. Read them before starting any firmware work.
| # | Lesson | Impact |
|---|---|---|
| L1 | UART1 is the ground station interface — use UART0 for Pico | Critical |
| L2 | Custom commands must be in board.c — not a separate file |
Critical |
| L3 | Never reinitialize reply->header in custom_commands |
Critical |
| L4 | WATCHDOG_CLEAR corrupts RF receive — never use it |
High |
| L5 | uint32_t causes XDATA RAM crash — use uint16_t only |
High |
| L6 | uint16_t wraps at 65535 — use nested loops for long timeouts |
High |
| L7 | Zero-length ESP payload silently rejected by RX ISR | Medium |
| L8 | SD mount corrupts SPI state for Arducam — reinit SPI after mount | High |
| L9 | Arducam needs direct wire connection — breadboard adds too much capacitance | Medium |
| L10 | Camera init blocks UART — defer to first idle timeout, never at boot | Medium |
| L11 | Ground unit must run original unmodified OpenLST firmware | High |
| L12 | After direct serial flash: send 2–3 get_telemetry before RF ops |
Medium |
| L13 | Always Clean + Build — stale .rel files cause silent firmware bugs |
High |
| L14 | SPI bus shared between camera and SD — always deselect both CS before switching | High |
| L15 | Prototype board SPI limited to 400kHz — jumper wire noise corrupts at higher speeds | Medium |
| L16 | PICO_TIMEOUT values tuned for production PCB — double them on prototype board | Medium |
| L17 | RF_PA_CONFIG must appear exactly once in board.h — ground station patching tool requires it |
High |
| L18 | Bootloader window is 600ms — ground station must detect beacon and respond within this window | High |
SpaceCommsKit Developer Guide v2.0 For updates and latest version see https://spacecommskit.com/docs