FPGA: Gowin GW1N-UV2 (non-volatile) MCU: Artery AT32F403A (ARM Cortex-M4F @ 240MHz) Firmware analyzed: V1.2.0 binary, Ghidra decompilation Last updated: 2026-04-01
The FPGA uses a dual-interface architecture with two completely independent communication channels running simultaneously:
| Interface | Purpose | Speed | Pins |
|---|---|---|---|
| USART2 | Command/control | 9600 baud | PA2 (TX), PA3 (RX) |
| SPI3 | Bulk ADC data | 60 MHz | PB3 (SCK), PB4 (MISO), PB5 (MOSI), PB6 (CS) |
Why two interfaces? The USART2 channel is a low-bandwidth, reliable command channel for configuration, mode switching, and meter data. It runs at 9600 baud because reliability matters more than speed for control messages. The SPI3 channel is a high-bandwidth data pipe for streaming raw ADC samples at 60 MHz -- fast enough to move 250MS/s waveform data in real time. Separating control from data means the MCU can reconfigure the FPGA (voltage range, trigger level, timebase) without interrupting the ADC data stream.
Additional control signals:
| Pin | Function | Active State |
|---|---|---|
| PC6 | SPI3 enable gate | HIGH to enable FPGA SPI3 |
| PB11 | FPGA active mode | HIGH during active measurement |
Both pins must be HIGH before SPI3 will respond with real data. Missing either one is the most common reason SPI3 returns 0xFF.
- Pins: PA2 (MCU TX to FPGA), PA3 (MCU RX from FPGA)
- Baud rate: 9600
- Format: 8N1 (8 data bits, no parity, 1 stop bit)
- Peripheral base:
0x40004400
USART2 is not free-running. It is driven by a TMR3 interrupt (IRQ 29, timer base 0x40000400). The TMR3 ISR fires periodically and calls the usart2_irq_handler at 0x080277B4, which pumps TX bytes out one at a time and collects RX bytes into a frame buffer. This timer-paced approach is critical -- polling or interrupt-driven USART without the timer cadence will not work correctly because the FPGA expects specific inter-byte timing.
The TMR3 ISR also triggers button scanning via input_and_housekeeping, so stopping TMR3 halts both USART communication and button input.
The TX buffer lives at 0x20000005 in RAM. The TX byte index at 0x2000000F tracks which byte the ISR should send next.
Byte [0]: Header byte 0 (0x00 from BSS init)
Byte [1]: Header byte 1 (0x00 from BSS init)
Byte [2]: Command high byte (cmd_hi)
Byte [3]: Command low byte (cmd_lo)
Byte [4]: Device ID / fixed value (from DAT_20000008)
Byte [5]: Parameter 1 (context-dependent)
Byte [6]: Parameter 2 (context-dependent)
Byte [7]: Parameter 3 (context-dependent)
Byte [8]: Trailer (0xAA)
Byte [9]: Checksum = (cmd_item + cmd_hi) & 0xFF
The usart_tx_frame_builder task (0x080373F4, dvom_TX FreeRTOS task) receives 2-byte command items from USART_TX_QUEUE (0x20002D74, 10-deep, 2-byte items), fills the TX buffer, and enables the TX interrupt (TDBEIEN bit in USART2_CTRL1). After each frame, it calls vTaskDelay(10) (10ms) before accepting the next command. This enforces a minimum 10ms gap between consecutive TX frames.
The RX buffer lives at 0x20004E11 (12 bytes). The RX byte index at 0x20004E10 tracks incoming bytes.
Two frame types:
| Type | Header | Total Length | Purpose |
|---|---|---|---|
| Data frame | 0x5A 0xA5 |
12 bytes | Meter data, ADC status, button state |
| Echo frame | 0xAA 0x55 |
10 bytes | Command acknowledgment |
Data frame (0x5A 0xA5):
Byte [0]: 0x5A (header)
Byte [1]: 0xA5 (header)
Bytes [2-6]: BCD-encoded meter digits (cross-byte nibble pairs)
Byte [7]: Status flags (AC, auto-range, overload, polarity)
Byte [8]: Measurement flags
Byte [9]: Additional status
Bytes [10-11]: Extra data (big-endian 16-bit, used for range info)
On receiving a complete 12-byte data frame, the USART ISR:
- Sends to queue
0x20002D7C(meter semaphore) - Triggers PendSV (
SCB_ICSR |= (1 << 28)) for immediate context switch
Echo frame (0xAA 0x55):
Byte [0]: 0xAA (header)
Byte [1]: 0x55 (header)
Bytes [2-9]: Echo data
Validation: rx[3] must match tx[3] (command echo) and rx[7] must be 0xAA. Echo frames do not trigger queue sends -- they are for validation only.
Captured idle heartbeat example: 5A A5 E4 2E 63 25 07 00 00 00 00 xx
uint8_t checksum = (cmd_item + cmd_high_byte) & 0xFF;Where cmd_item is the full 16-bit queue item (high byte = command, low byte = parameter) and cmd_high_byte is byte [2] of the TX frame.
- SCK: PB3 (Alternate function push-pull, 50MHz)
- MISO: PB4 (Input floating)
- MOSI: PB5 (Alternate function push-pull, 50MHz)
- CS: PB6 (General purpose output push-pull, software controlled, active LOW)
- Peripheral base:
0x40003C00
Important: PB3, PB4, and PB5 are JTAG pins by default. The AFIO remap register must be set to disable JTAG-DP while keeping SW-DP:
AFIO_PCF0 = (AFIO_PCF0 & ~0xF000) | 0x2000; // JTAG-DP disabled, SW-DP enabledThis frees PB3/PB4/PB5 for SPI3 use. The IOMUX/AFIO clock must be enabled first.
| Parameter | Value | Register Bits |
|---|---|---|
| Mode | 3 (CPOL=1, CPHA=1) | Clock idle HIGH, sample on falling edge |
| Role | Master | MSTR=1 |
| Data frame | 8-bit | DFF=0 |
| Bit order | MSB first | LSBFIRST=0 |
| NSS management | Software | SSM=1, SSI=1 |
| Baud rate | /2 prescaler | BR[2:0]=000 |
| Clock speed | 60 MHz | APB1 (120MHz) / 2 |
| Duplex | Full duplex | BIDIMODE=0 |
Post-init register writes:
SPI3_CTL1 |= 0x02; // Bit 1: TXDMAEN or RXNEIE
SPI3_CTL1 |= 0x01; // Bit 0: RXDMAEN or TXEIE
SPI3_CTL0 |= 0x40; // Bit 6: SPE (SPI enable)| Pin | Register | Assert | Deassert | Purpose |
|---|---|---|---|---|
| PB6 (CS) | GPIOB BOP/BCR (0x40010C10/0x40010C14) |
BCR = 0x40 (LOW) |
BOP = 0x40 (HIGH) |
Chip select |
| PC6 (Enable) | GPIOC BOP | BOP = (1<<6) (HIGH) |
-- | Must be HIGH for FPGA SPI to function |
| PB11 (Active) | GPIOB BOP | BOP = 0x800 (HIGH) |
-- | Must be HIGH during measurement |
Every SPI3 exchange follows this sequence:
1. CS ASSERT: GPIOB_BCR = 0x40 (PB6 LOW)
2. Wait TXE: poll SPI3_STAT bit 1 (TX buffer empty)
3. Write: SPI3_DATA = command_byte
4. Wait RXNE: poll SPI3_STAT bit 0 (RX buffer not empty)
5. Read: response = SPI3_DATA
6. [Repeat steps 2-5 for additional data bytes, sending 0xFF to clock out]
7. CS DEASSERT: GPIOB_BOP = 0x40 (PB6 HIGH)
The TXE/RXNE polling in the stock firmware uses an optimized pattern: it checks the status register 3 times in conditional IT blocks before falling into a polling loop, reducing latency for the common fast-response case.
MCU FPGA
| |
| CS_ASSERT (PB6 LOW) |
| |
| --> command byte |
| <-- status/data byte | (simultaneous full-duplex)
| |
| --> 0xFF (clock out) |
| <-- data byte 1 |
| |
| --> 0xFF (clock out) |
| <-- data byte 2 |
| ... |
| |
| CS_DEASSERT (PB6 HIGH) |
Complete power-on to first SPI3 data, based on decompilation of system_init (FUN_08023A50, ~15.4KB) and spi3_init_and_setup (0x08039050, 312 bytes).
Step 1. Enable peripheral clocks: GPIOA, GPIOB, GPIOC, GPIOD, GPIOE, AFIO
Step 2. Configure PC9 as output, set LOW (power hold -- main() sets HIGH)
Step 3. Configure 47 GPIO pins total:
- Analog front-end MUX pins
- LCD parallel bus (EXMC)
- Button matrix inputs
- SPI flash (SPI2) pins
- USB pins
- Debug UART pins
Step 4. AFIO remap: AFIO_PCF0 = (AFIO_PCF0 & ~0xF000) | 0x2000
-> JTAG-DP disabled, SW-DP enabled
-> Frees PB3/PB4/PB5 from JTAG for SPI3 use
Step 5. Enable NVIC IRQ9 (EXTI9_5 or DMA)
Step 6. Enable USART clock: RCU_APB1EN |= 0x20000 (bit 17 = USART2)
Step 7. Configure TIM5 (0x40000C00): timer_init() with APB1-derived prescaler
Step 8. Configure DMA0: clear channel 0 flags
Step 9. Configure USART2 (0x40004400): 9600 baud, 8N1, TX+RX enabled
Step 10. Initialize meter_state (0x200000F8): load defaults from data block
- Validates data block header (0x55 or 0xAA signature)
- Copies calibration values, range settings, mode defaults
Step 11. Create 7 queues:
0x20002D6C: xQueueCreate(20, 1) -- USART command queue
0x20002D70: xQueueCreate(15, 1) -- button event queue
0x20002D74: xQueueCreate(10, 2) -- USART TX queue (2-byte items)
0x20002D78: xQueueCreate(15, 1) -- SPI3 data trigger queue
0x20002D7C: xQueueCreate(1, 0) -- binary semaphore (meter RX complete)
0x20002D80: xQueueCreate(1, 0) -- binary semaphore (SPI3 init sync)
0x20002D84: xQueueCreate(1, 0) -- binary semaphore (EXTI notification)
Step 12. Create 8 FreeRTOS tasks:
"Timer1" @ 0x080400B9 prio=10 stack=10w
"Timer2" @ 0x080406C9 prio=1000 stack=1000w
"display" @ 0x0803DA51 prio=1 stack=384w (1536B)
"key" @ 0x08040009 prio=4 stack=128w (512B)
"osc" @ 0x0804009D prio=2 stack=256w (1024B)
"fpga" @ 0x0803E455 prio=3 stack=128w (512B)
"dvom_TX" @ 0x0803E3F5 prio=2 stack=64w (256B)
"dvom_RX" @ 0x0803DAC1 prio=3 stack=128w (512B)
Step 13. Signal semaphore at 0x20002D80
Step 14. Suspend tasks at 0x20002D7C and 0x20002D84
Step 15. Send initialization commands to FPGA via USART2 (inline, NOT via queues)
Format: 10-byte frames transmitted byte-by-byte with TX polling
Commands sent in sequence: 0x01, 0x02, 0x06, 0x07, 0x08
These configure the FPGA for initial oscilloscope mode before the
scheduler starts.
Critical note: These boot commands are sent by direct register polling, not through the FreeRTOS queue/task infrastructure, because the scheduler has not started yet.
Step 16. Enable SPI3 clock: RCU_APB1EN |= 0x8000 (bit 15)
Step 17. Enable RCU_APB2EN bits for AFIO (bit 0 = AFEN)
Step 18. Configure SPI3 GPIO:
PB3 = SPI3_SCK (AF push-pull, 50MHz)
PB4 = SPI3_MISO (input floating)
PB5 = SPI3_MOSI (AF push-pull, 50MHz)
PB6 = SPI3_CS (GPIO output push-pull)
Step 19. *** PC6 = FPGA SPI enable (output push-pull, set HIGH) ***
GPIOC_BOP = (1 << 6)
Step 20. Configure GPIOC pin for SPI3 (via gpio_init at 0x08026634)
Step 21. SPI3 peripheral init:
Full duplex, Master, /2 clock, MSB first, 8-bit
CPOL=1, CPHA=1 (MODE 3)
Software NSS (SSM=1, SSI=1)
Step 22. Post-init:
SPI3_CTL1 |= 0x02 (TXDMAEN or RXNEIE)
SPI3_CTL1 |= 0x01 (RXDMAEN or TXEIE)
SPI3_CTL0 |= 0x40 (SPE = SPI enable)
Step 23. Enable more APB2 clocks: RCU_APB2EN |= 0x10
Step 24. SysTick delay #1: LOAD = *(0x20002B1C) * 10, wait for COUNTFLAG
Step 25. SysTick delay #2: LOAD = *(0x20002B20), wait for COUNTFLAG
Step 26. Timed loop (2 iterations):
Counter starts at 100 (0x64)
First: if count >= 51 -> LOAD = reload * 50, count -= 50
Second: if count < 51 -> LOAD = reload * count, count = 0
Each iteration: start SysTick, wait for COUNTFLAG, stop
These delays are timing-critical. The FPGA needs specific setup time after SPI3 is enabled before the handshake can proceed.
Step 27. CS DEASSERT: GPIOB_BOP = 0x40 (PB6 HIGH)
Step 28. SPI3 send 0x00 (dummy, CS high) -- flush bus
Step 29. Read SPI3 response (discard)
Step 30. CS ASSERT: GPIOB_BCR = 0x40 (PB6 LOW)
Step 31. SPI3 send 0x05 -- FPGA COMMAND (status/ID query)
Step 32. Read SPI3 response (FPGA status/ID)
Step 33. SPI3 send 0x00 (parameter, still CS low)
Step 34. Read SPI3 response
Step 35. CS DEASSERT: GPIOB_BOP = 0x40 (PB6 HIGH)
Step 36. SPI3 send 0x00 (dummy, CS high) -- flush
Step 37. Read response (discard)
Step 38. SPI3 send 0x00 (dummy)
Step 39. Read response (discard)
Step 40. Another SysTick delay loop (100 -> 50 -> 0, same as step 26)
Step 41. CS ASSERT again
Step 42. More SPI3 exchanges (command 0x12 and others)
Step 43. Configure ADC sampling parameters
Step 44. Set up trigger levels and timebase
Step 45. Initialize display buffer pointers
Step 46. DMA configuration for SPI3 bulk transfers
Step 47. Timer6/Timer7 configuration
Step 48. Meter measurement function init
Step 49. Watchdog timer init (~5 sec timeout, IWDG)
Step 50. Enable TMR3 (IRQ 29) -- THIS DRIVES THE USART EXCHANGE
Step 51. Enable TMR6/TMR7
Step 52. *** PB11 set HIGH *** (FPGA active mode signal)
GPIOB_BOP = 0x800
(Set in mode_switch_reset_handler, late in boot)
Step 53. Start FreeRTOS scheduler (tail-call to 0x0803A6D8)
Step 54. TMR3 ISR fires periodically -> calls usart2_irq_handler()
Step 55. USART handler pumps TX bytes / receives RX bytes
Step 56. On valid 12-byte RX frame (0x5A 0xA5 header):
-> sends to queue 0x20002D7C
-> triggers PendSV for context switch
Step 57. FPGA task processes queue items:
-> usart_cmd_dispatcher processes commands
-> spi3_acquisition_task waits on queue 0x20002D78
-> When triggered, does SPI3 transfers (9 command types)
Step 58. SPI3 data stored at meter_state+0x5B0 (CH1) and +0x9B0 (CH2)
USART commands flow through a multi-level system:
- Application layer calls mode-specific functions that queue 1-byte command codes to
usart_cmd_queue(0x20002D6C) usart_cmd_dispatcher(0x08036A50) receives these and dispatches through a function pointer table at0x0804BE74- Dispatch handlers call
usart_tx_config_writer(0x08039734) which formats 2-byte items and queues them tousart_tx_queue(0x20002D74) usart_tx_frame_builder(0x080373F4, dvom_TX task) dequeues items, fills the 10-byte TX frame, and triggers transmission
| Code | Direction | Purpose | Blocking | Notes |
|---|---|---|---|---|
0x00 |
TX | Reset/Init | No | Sent at boot and on mode transitions |
| Code | Direction | Purpose | Config Writer Type | Blocking |
|---|---|---|---|---|
0x01 |
TX | Configure channel (generic) | Type 0 (CH1) or Type 1 (CH2) | No |
0x0B |
TX | Scope channel config | Types 0-3 | No |
0x0C |
TX | Scope channel config | Types 0-3 | No |
0x0D |
TX | Scope channel config | Types 0-3 | No |
0x0E |
TX | Scope channel config | Types 0-3 | No |
0x0F |
TX | Scope channel config | Types 0-3 | No |
0x10 |
TX | Scope channel config | Types 0-3 | No |
0x11 |
TX | Scope channel config | Types 0-3 | No |
| Code | Direction | Purpose | Blocking | Notes |
|---|---|---|---|---|
0x16 |
TX | Trigger: threshold LSB | No | Sent as part of 5-command sequence |
0x17 |
TX | Trigger: threshold MSB | No | |
0x18 |
TX | Trigger: mode/edge select | No | |
0x19 |
TX | Trigger: holdoff | Yes (portMAX_DELAY) | Final in sequence, blocks until accepted |
| Code | Direction | Purpose | Blocking | Notes |
|---|---|---|---|---|
0x1A |
TX | Channel: CH1 gain | No | Part of 6-command sequence |
0x1B |
TX | Channel: CH1 offset | No | |
0x1C |
TX | Channel: CH2 gain | No | |
0x1D |
TX | Channel: CH2 offset | No | |
0x1E |
TX | Channel: coupling/BW limit | Yes (portMAX_DELAY) | Final in sequence |
| Code | Direction | Purpose | Blocking | Notes |
|---|---|---|---|---|
0x26 |
TX | Timebase: prescaler | No | Part of 3-command sequence |
0x27 |
TX | Timebase: period | No | |
0x28 |
TX | Timebase: mode | Yes (portMAX_DELAY) | Final in sequence |
| Code | Direction | Purpose | Blocking | Notes |
|---|---|---|---|---|
0x20 |
TX | Acquisition: run mode | No | Part of 2-command sequence |
0x21 |
TX | Acquisition: sample depth | Yes (portMAX_DELAY) | Final in sequence |
| Code | Direction | Purpose | Config Writer Type | Blocking |
|---|---|---|---|---|
0x07 |
TX | Probe/tail byte (PC7 HIGH branch) | Type-4-shaped bitfield | No |
0x08 |
TX | Meter command-bank byte | Type-4-shaped bitfield | No |
0x09 |
TX | Meter: start measurement | -- | No |
0x0A |
TX | No probe detected (PC7 LOW) | -- | No |
0x12 |
TX | Meter variant setup | Type 4 | No |
0x13 |
TX | Meter variant setup | Type 4 | No |
0x14 |
TX | Meter variant setup | Type 4 | No |
0x16 |
TX | Meter command-bank byte | Type-4-shaped bitfield | No |
0x17 |
TX | Meter command-bank byte | Type-4-shaped bitfield | No |
0x18 |
TX | Meter command-bank byte | Type-4-shaped bitfield | No |
0x19 |
TX | Meter command-bank byte | Type-4-shaped bitfield | No |
0x1A |
TX | Meter command-bank byte | Type-4-shaped bitfield | No |
0x1B |
TX | Meter display/update byte | Type-4-shaped bitfield | No |
0x1C |
TX | Meter command-bank byte | Type-4-shaped bitfield | No |
0x1D |
TX | Meter display/update byte | Type-4-shaped bitfield | No |
0x1E |
TX | Meter command-bank byte | Type-4-shaped bitfield | No |
Legacy correction (2026-06-06): the Type-4-shaped arm and command-bank
bytes above are not proof of normal DMM runtime range configuration. Current
stock guards recover boot/runtime mode-init command banks, selector raw words,
and transport reset/resume/drain behavior, but still do not recover a
DMM-owned runtime analog writer for ms[0x02]/ms[0x03], exact settle/discard
timing, or a low-DCV calibration source. Do not use this legacy command table
to derive magnitude-based relay/range feedback.
| Code | Direction | Purpose | Config Writer Type | Blocking |
|---|---|---|---|---|
0x02 |
TX | Siggen setup | Type 5 (freq) | No |
0x03 |
TX | Siggen setup | Type 6 (wave) | No |
0x04 |
TX | Siggen setup | -- | No |
0x05 |
TX | Siggen setup | -- | No |
0x06 |
TX | Siggen setup | -- | No |
| Code | Direction | Purpose | Blocking | Notes |
|---|---|---|---|---|
0x07 |
TX | Probe attenuation (param < 0) | No | Conditional on parameter sign |
0x0A |
TX | Probe attenuation (param >= 0) | No | Conditional on parameter sign |
| Code | Direction | Purpose | Mode | Notes |
|---|---|---|---|---|
0x15 |
TX | Standalone mode 7 | 7 | Single command |
0x1F |
TX | Mode 4 config | 4 | Unknown payload |
0x20 |
TX | Mode 4 config | 4 | -- |
0x21 |
TX | Mode 4 config | 4 | -- |
0x25 |
TX | Mode 5 config | 5 | Unknown payload |
0x26 |
TX | Mode 5 config | 5 | -- |
0x27 |
TX | Mode 5 config | 5 | -- |
0x28 |
TX | Mode 5 config | 5 | -- |
0x29 |
TX | Standalone mode 6 | 6 | Single command |
0x2C |
TX | Mode 8 config | 8 | -- |
The mode dispatcher at FUN_0800735C selects which command sequences to send based on the active mode:
| Mode | Name | Command Sequence |
|---|---|---|
| 0 | Oscilloscope | 0x00, 0x01, 0x0B-0x11 |
| 1 | Multimeter (basic) | 0x00, 0x09, 0x07/0x0A, 0x1A-0x1E |
| 2 | Signal Generator | 0x02-0x06, 0x08, then mode 9 tail |
| 3 | Multimeter (extended) | 0x00, 0x08, 0x09, 0x07/0x0A, 0x16-0x19 |
| 4 | Unknown | 0x00, 0x1F, 0x09, 0x20, 0x21 |
| 5 | Unknown | 0x00, 0x25, 0x09, 0x26, 0x27, 0x28 |
| 6 | Standalone | 0x29 |
| 7 | Standalone | 0x15 |
| 8 | Unknown | 0x00, 0x2C |
| 9 | Multimeter (variant) | 0x00, 0x12, 0x13, 0x14, then 0x09, 0x07/0x0A |
For modes 1, 3, and 9: command 0x07 is sent if PC7 is HIGH (probe detected), otherwise 0x0A is sent.
Multi-command sequences use a specific blocking pattern from the gap functions:
// Example: set trigger config (FUN_0800bb10)
xQueueGenericSend(queue, &cmd_0x07_or_0x0A, 0, 0); // Non-blocking
xQueueGenericSend(queue, &cmd_0x16, 0, 0); // Non-blocking
xQueueGenericSend(queue, &cmd_0x17, 0, 0); // Non-blocking
xQueueGenericSend(queue, &cmd_0x18, 0, 0); // Non-blocking
xQueueGenericSend(queue, &cmd_0x19, portMAX_DELAY, 0); // BLOCKS until acceptedThe final command in every sequence uses portMAX_DELAY (0xFFFFFFFF) to block the calling task until the queue accepts it. This ensures all commands in the batch are enqueued before the caller proceeds.
The spi3_acquisition_task (0x08037454, 7208 bytes) is the heart of the data pipeline. It waits on spi3_data_queue (0x20002D78) for trigger events, then executes one of 9 acquisition modes selected by (trigger_byte - 1).
Important: The trigger is double-buffered. When the acquisition counter in input_and_housekeeping reaches the timebase threshold, TWO items are sent to the SPI3 queue back-to-back. This triggers two consecutive read cycles, implementing a ping-pong scheme that prevents display tearing.
trigger_byte - 1
|
+---------------+-------------------------------+
| | |
+----v----+ +------v------+ +----------+ +------v--+
| Case 0 | | Case 1 | | Case 2 | | Case 3 |
| FAST TB | | ROLL MODE | | NORMAL | | DUAL |
| config | | circ buf | | 1024 B | | 2048 B |
| only | | 300 samp | | interl. | | CH1+CH2 |
+---------+ +------------+ +----------+ +---------+
+---------+ +---------+ +----------+ +----------+ +----------+
| Case 4 | | Case 5 | | Case 6 | | Case 7 | | Case 8 |
|EXTENDED | |METER ADC| | SIGGEN | |CALIBRATE | |SELF TEST |
| cmd only| | ch read | | feedback | | 16-bit | | verify |
+---------+ +---------+ +----------+ +----------+ +----------+
Purpose: Configures FPGA for fast sweep speeds (timebase index < 4). No ADC data is read.
Trigger condition: trigger_byte == 1
Protocol:
CS_ASSERT
spi3_xfer(timebase_index) // ms[0x2D], range 0x00-0x13
CS_DEASSERT
Logic:
uint8_t tb_idx = ms[0x2D];
uint8_t sample_threshold = TIMEBASE_SAMPLE_TABLE[tb_idx] + 0x32;
uint16_t acq_count = *(uint16_t *)(ms + 0xDB8);
if (sample_threshold <= acq_count) {
// Acquisition complete, skip SPI transfer
return;
}
spi3_xfer(tb_idx); // Configure FPGA timebaseThe timebase-to-sample-count lookup table lives at 0x0804D833 in flash.
Purpose: Continuous scrolling oscilloscope display. Reads one sample pair per trigger and appends to a 300-sample circular buffer.
Trigger condition: trigger_byte == 2
Byte count: 5 bytes per trigger (2 per channel + 1 extra)
Protocol:
CS_ASSERT
spi3_xfer(0xFF) -> raw_ch1_hi (reference)
spi3_xfer(0xFF) -> raw_ch1_lo (data)
spi3_xfer(0xFF) -> raw_ch2_hi (data)
spi3_xfer(0xFF) -> raw_ch2_lo (extra)
spi3_xfer(0xFF) -> last_byte -> stored at ms[0x5AF]
CS_DEASSERT
Buffer management:
- CH1 circular buffer:
ms + 0x356(301 bytes,0x12D) - CH2 circular buffer:
ms + 0x483(301 bytes,0x12D) - Read pointer:
ms + 0xDB4(16-bit) - Sample count:
ms + 0xDB6(16-bit, max 300 =0x12C) - Roll calibrated CH1:
ms + 0x482 - Roll calibrated CH2:
ms + 0x5AF
Circular buffer algorithm:
- If
read_ptr > 0, decrement it - If
sample_count <= 300, increment it - Shift all existing samples down by 1 position (expensive memmove)
- Read new samples via SPI3
- Apply VFP calibration
- Store calibrated values at the head of the buffer
Display trigger: When sample_count == 0x12D (301):
- Check
hold_modeandtrigger_mode - Disable TMR3 (temporarily stop USART polling)
- Send command 2 to USART cmd queue
- Call
measurement_calc()at0x08034078 - Copy circular buffer to main ADC buffers
Purpose: Standard oscilloscope capture. Reads 1024 bytes of interleaved CH1/CH2 data.
Trigger condition: trigger_byte == 3
Byte count: 1024 bytes (0x400)
Protocol:
CS_ASSERT
spi3_xfer(0xFF) // Initial command byte (read all)
// Tight loop, 1024 bytes:
for i = 0 to 0x3FF step 2:
spi3_xfer(0xFF) -> CH1 byte -> ms[0x5B0 + i]
spi3_xfer(0xFF) -> CH2 byte -> ms[0x5B0 + i + 1]
CS_DEASSERT
Data format: Interleaved unsigned 8-bit samples:
Byte 0: CH1 sample [0]
Byte 1: CH2 sample [0]
Byte 2: CH1 sample [1]
Byte 3: CH2 sample [1]
...
Byte 1022: CH1 sample [511]
Byte 1023: CH2 sample [511]
Buffer: All 1024 bytes stored contiguously at ms + 0x5B0.
After the transfer completes, falls through to VFP calibration post-processing (section 7).
Purpose: Extended dual-channel capture. Reads 2048 bytes, split into separate CH1 and CH2 buffers.
Trigger condition: trigger_byte == 4
Byte count: 2048 bytes (0x800)
Protocol: Same as case 2 but double the data:
CS_ASSERT
spi3_xfer(0xFF) // Initial command
// 2048 bytes interleaved:
for i = 0x400 to 0x7FF step 2:
spi3_xfer(0xFF) -> ms[0x5B0 + i]
spi3_xfer(0xFF) -> ms[0x5B0 + i + 1]
CS_DEASSERT
Buffers:
- CH1:
ms + 0x5B0(1024 bytes) - CH2:
ms + 0x9B0(1024 bytes)
After transfer, falls through to calibration at 0x0803806C.
Purpose: Sends a specific command byte to the FPGA without reading bulk data. Used for XY mode, deep memory, or configuration commands.
Trigger condition: trigger_byte == 5
Byte count: 1 (command only)
Protocol:
CS_ASSERT
spi3_xfer(command_code) // Transformed voltage range
CS_DEASSERT
Stores response to ms + 0x46 (trigger status word).
Purpose: Reads ADC value for multimeter mode from the specified channel.
Trigger condition: trigger_byte == 6
Byte count: 1
Protocol:
CS_ASSERT
spi3_xfer(active_channel) // ms[0x16], value 0 or 1
CS_DEASSERT
Purpose: Monitors signal generator output by reading feedback from the FPGA.
Trigger condition: trigger_byte == 7
Byte count: 1
Protocol:
CS_ASSERT
spi3_xfer(trigger_mode) // ms[0x18]
CS_DEASSERT
Purpose: Multi-phase SPI3 exchange for reading FPGA internal status, calibration data, or version info. Returns a 16-bit assembled value.
Trigger condition: trigger_byte == 8
Byte count: 5 (across two CS cycles)
Protocol:
Phase 1:
CS_ASSERT
spi3_xfer(command_code) -> high_byte
CS_DEASSERT
vTaskDelay(1) // 1ms pause between phases
Phase 2:
CS_ASSERT
spi3_xfer(0x0A) -> status (discarded)
spi3_xfer(0xFF) -> low_byte_1
spi3_xfer(0xFF) -> low_byte_2
CS_DEASSERT
Result assembly:
ms[0x46] = (high_byte << 8) | low_byte; // 16-bit status word
ms[0xDB0]++; // Increment frame counterPurpose: FPGA communication integrity test. Used during boot validation.
Trigger condition: trigger_byte == 9
Byte count: 3
Protocol:
CS_ASSERT
spi3_xfer(command_code)
spi3_xfer(0xFF)
spi3_xfer(0xFF)
CS_DEASSERT
- Encoding: Unsigned 8-bit (0-255)
- Interleaving: CH1 and CH2 alternate byte-by-byte in normal mode
- DC offset: The FPGA ADC has a hardware DC offset of approximately 28 LSBs
These registers are loaded once when spi3_acquisition_task enters its main loop and remain valid for the lifetime of the task:
| VFP Register | Value | Purpose |
|---|---|---|
s16 |
-28.0 |
ADC zero offset (hardware DC offset correction) |
s18 |
(literal pool) | Calibration gain CH1 |
s20 |
(literal pool) | Calibration offset CH2 |
s22 |
(literal pool) | DC bias correction |
s24 |
(literal pool) | Maximum clamp value (255.0) |
s26 |
(literal pool) | Minimum clamp value (0.0) |
s28 |
(literal pool) | Range divisor |
Literal pool location: 0x08037674+ in flash.
Applied to every raw 8-bit ADC sample after bulk SPI3 read (cases 2 and 3):
float raw_f = (float)(uint8_t)raw_sample;
float norm = (raw_f + s16_offset) / s28_divisor; // Normalize to 0..1
int8_t dc_off = meter_state[0x04]; // Per-channel DC offset (signed)
float range = (float)((int16_t)voltage_range - dc_off); // Display range
float result = norm * range + (float)dc_off + s22_bias; // Scale to display coords
// Clamp to valid pixel range
if (result > s24_max) result = s24_max; // 255.0
if (result < 0.0f) result = s26_min; // 0.0
calibrated = (uint8_t)(int)result;Conditions for calibration to run:
spi3_transfer_mode(ms[0x14]) must not be 3timebase_index(ms[0x2D]) must be < 4calibration_mode(ms[0x33]) must be 0 (not calibrating)- Both probe types (
ms[0x352],ms[0x353]) must be valid (not 0 or 0xFF)
Applied in case 1 when probe type value exceeds 0xDC (220):
float probe_f = (float)probe_type;
float probe_gain = (probe_f + s16_offset) / s18_gain;
float raw_f = (float)raw_byte;
float adjusted = (raw_f + s20_offset - (float)dc_offset) / probe_gain;
float result = adjusted + s22_bias + (float)dc_offset;
// Clamp
if (result > s24_max) result = s24_max;
if (result < s26_min) result = s26_min;
calibrated = (uint8_t)(int)result;| Offset | Size | Purpose |
|---|---|---|
+0x356 |
301 bytes | Roll mode circular buffer CH1 |
+0x482 |
1 byte | Roll mode calibrated sample CH1 |
+0x483 |
301 bytes | Roll mode circular buffer CH2 |
+0x5AF |
1 byte | Roll mode last byte / calibrated CH2 |
+0x5B0 |
1024 bytes | ADC sample buffer CH1 |
+0x9B0 |
1024 bytes | ADC sample buffer CH2 |
+0xDB0 |
1 byte | SPI3 frame counter |
+0xDB4 |
2 bytes | Roll mode read pointer |
+0xDB6 |
2 bytes | Roll mode sample count (max 300) |
+0xDB8 |
2 bytes | Acquisition sample counter |
⚠ Corrected 2026-08-14. This section used to read "per-channel calibration data (301 bytes each) is loaded from SPI flash at boot by
calibration_loader". Both halves are wrong: the regions are the oscilloscope roll-mode sample buffers (analysis_v120/cal_data_myth_busted.md), and no calibration file has ever been found on the SPI flash (analysis_v120/w25q128_flash_map_2026-06-13.md).
FUN_08001830 (flash 0x08001830) pre-seeds the two 301-byte roll-mode buffers at boot:
- CH1:
ms + 0x356, count0x12D, keystate[4] ^ 0x80 - CH2:
ms + 0x483, count0x12D, keystate[5] ^ 0x80
Separately, the scope gain/offset table at RAM 0x20000358–0x20000434 (20 bytes per
voltage range; entry count inconsistent across our docs) is real, but stock restores it
from a saved config with a sentinel at ms[0x34E], falling back to defaults compiled
into the stock image at 0x080261BE..0x08026506. Where (or whether) per-device factory
calibration is stored is an open question — analysis_v120/factory_cal_truth_2026-08-14.md.
The meter_data_processor (0x08036AC0, 1776 bytes) runs as a FreeRTOS task that:
- Blocks on
METER_SEMAPHORE(0x20002D7C) until USART ISR signals a complete RX frame - Extracts BCD digits from RX frame bytes [2..6]
- Applies calibration (double-precision floating point)
- Classifies the result and formats for display
USART RX frame bytes [2..6] contain cross-byte nibble pairs. Each pair is looked up in a command lookup table to produce a decimal digit (0-9) or special code:
uint8_t b2 = rx[2], b3 = rx[3], b4 = rx[4], b5 = rx[5], b6 = rx[6];
digit0 = lookup_table[(b2 & 0xF0) | (b3 & 0x0F)];
digit1 = lookup_table[(b3 & 0xF0) | (b4 & 0x0F)];
digit2 = lookup_table[(b4 & 0xF0) | (b5 & 0x0F)];
digit3 = lookup_table[(b5 & 0xF0) | (b6 & 0x0F)];| Pattern | Meaning | Action |
|---|---|---|
digit0=0x0A, digit1=0x0B |
"OL" -- Overload | Display "OL" |
digit0=0x10, digit1=0x10 |
Blank display | No measurement active |
digit0=0x10, digit1=0x11 |
Partial blank | Transitional state |
digit1=0x12, digit2=0x0A, digit3=5 |
Continuity detected | Sets buzzer state ms[0xF5D] = 0xB1 |
digit1=0x13, digit2=0x14 |
Mode change indicator | Mode transition in progress |
Any digit = 0xFF |
Invalid/unrecognized | Skip measurement |
For valid digits, values >= 0x10 have 0x10 subtracted:
int d0 = (digit0 >= 0x10) ? (digit0 - 0x10) : digit0;
int d1 = (digit1 >= 0x10) ? (digit1 - 0x10) : digit1;
int d2 = (digit2 >= 0x10) ? (digit2 - 0x10) : digit2;
int d3 = (digit3 >= 0x10) ? (digit3 - 0x10) : digit3;
int raw_value = d0 * 1000 + d1 * 100 + d2 * 10 + d3;The meter uses ARM EABI soft-float double-precision calls while preparing the
display value and decimal/formatter state. Older notes called ms[0xF37] a
calibration coefficient; current stock evidence identifies it as
DAT_2000102f, the display decimal-shift state. This block is not a recovered
factory calibration coefficient source.
// For each measurement channel (up to 4 iterations):
double divisor = (double)display_decimal_shift; // __aeabi_ui2d
double quotient = ref_value / divisor; // __aeabi_ddiv
double result = quotient * accumulated_scale; // __aeabi_dmul
int final = (int)result; // __aeabi_d2izVFP double constants from literal pool at 0x080373CC:
0.0(zero reference)494.0(calibration reference value)4503599627370496.0(2^52, used for integer conversion)
Stored at ms[0xF34]:
| Code | Meaning |
|---|---|
| 1 | Normal reading |
| 2 | Underrange (value too small for current range) |
| 3 | Overrange (exceeds display but not OL) |
| 4 | Invalid measurement |
The meter_sub_mode field at ms[0x10] selects the display format:
| Sub-Mode | Function | Decimal Offset |
|---|---|---|
| 0 | Voltage DC | digit_count + 0x0A |
| 1 | Voltage AC | Check polarity, +2 |
| 2 | Current | Check flags, +2 |
| 3 | (Unused) | 0xFF (invalid) |
| 4 | Resistance | Dual-channel conditional offset |
| 5 | Capacitance | digit_count + 2 |
| 6 | Frequency | digit_count + 0x0A |
| 7 | Diode/Continuity | Special handling |
The continuity buzzer is driven by EXTI3 interrupt handler at 0x08009C10. When the meter data processor detects the continuity BCD pattern:
ms[0xF5D] = 0xB1; // Activate buzzer
ms[0xF2D] = 1; // Update meter mode stateThe EXTI3 ISR reads ms[0xF5D] to drive the piezo buzzer. Values: 0xB0 = buzzer off, 0xB1 = buzzer on.
The meter_mode_handler (0x080371B0, 504 bytes) processes RX frame status bits to track the meter operating state. State variable: ms[0xF2D].
Input bits from RX frame:
rx[7](status byte): bit 0 participates in ACV display formatting, bit 1 = overload type 1, bit 2 remains a status/decimal helper and is not recovered as AC-present confidence, bit 3 = stock FSM status, bit 5 = ACV format pathrx[6](flags byte): bits 4/5 participate in stock FSM/display-state branches, bit 6 = hold/status helper; these are not recovered factory calibration coefficients
State transitions:
| State | Name | Entry Condition | Key Checks | Outputs |
|---|---|---|---|---|
| 0 | IDLE/INIT | Default | rx[7] bits 5,1,0,3 | Transitions to 1-5 |
| 1 | POLARITY/FORMAT | Overload/display branch | rx[7] bit 0 | Updates formatter/display state |
| 2 | OVERLOAD/RANGE-STATUS | Range/status branch | ms[0xF36], rx[7] bits 0,3 | Updates FSM/display state |
| 3 | AC/DC STATUS | Status branch | rx[7] bit 2, rx[6] bit 6 | Updates formatter/display state |
| 4 | RANGE-STATUS INDICATOR | Range/status readback | rx[6] bits 4,5, rx[7] bit 0 | Updates formatter/display state |
| 5 | AUTO-RANGE STATUS | Auto-range status active | ms[0xF39] flag | Updates formatter/display state |
| 6 | STANDBY | Idle/wait | rx[6] bit 4 | Updates formatter/display state |
| 7 | STANDBY | Idle/wait | rx[6] bit 4 | Updates formatter/display state |
Post-processing: if overload_flag == 2, extracts big-endian 16-bit value from rx[10..11] into ms[0xF3A].
The usart_tx_config_writer (0x08039734, 316 bytes) supports 7 command types that encode configuration parameters into the USART TX frame payload bytes.
Encodes CH1 voltage range, coupling, and bandwidth limit into TX frame.
Same format as Type 0 but for CH2.
config[0x20] bit assignments:
Bit 1: CH1 AC/DC coupling (0=DC, 1=AC)
Bit 3: CH1 bandwidth limit (0=full, 1=20MHz)
Bit 5: CH2 AC/DC coupling (0=DC, 1=AC)
Bit 7: CH2 bandwidth limit (0=full, 1=20MHz)
Bit 9: Trigger edge select (0=rising, 1=falling)
Bit 11: Trigger source select (0=CH1, 1=CH2)
config[0x18] voltage range encoding:
Bits [1:0]: Range low (2 bits)
Bits [15:12]: Range high (4 bits)
Formats trigger threshold, mode, edge, and holdoff values.
Formats timebase prescaler, period, and mode values.
Used for all meter commands (0x07, 0x08, 0x12-0x14, 0x16-0x1E). Encodes measurement range selection.
Encodes frequency value for signal generator output.
Encodes waveform type selection (sine, square, triangle, sawtooth, etc.).
Before sending to SPI3, the voltage range value is transformed:
int16_t command_code = ~0x7F ^ voltage_range; // Bitwise transform for FPGAThis produces the actual byte sent as the first SPI3 command after CS assert.
This section documents the investigation path. Every one of these was a real mistake that cost hours of debugging.
Root causes (all four must be addressed):
-
PB11 not set HIGH. The FPGA has an "active mode" signal on PB11. Without
GPIOB_BOP = 0x800, the FPGA keeps MISO pulled HIGH and ignores all SPI3 commands. PB11 is set late in the boot sequence (step 52) inside the mode switch handler, making it easy to miss. -
USART boot commands not sent. Before SPI3 will return real data, the FPGA must receive its initialization sequence over USART2: commands
0x01, 0x02, 0x06, 0x07, 0x08. These are sent inline (byte-by-byte TX polling) before the FreeRTOS scheduler starts. If you skip them, the FPGA is not configured and SPI3 data is meaningless. -
Trigger mechanism missing. SPI3 data reads are not initiated by polling. They are driven by queue events from
input_and_housekeeping, which sends items tospi3_data_queue(0x20002D78) when the acquisition counter reaches the timebase threshold. Without this timer-driven trigger, the acquisition task blocks forever onxQueueReceive. -
SysTick delays between boot phases. The FPGA needs specific timing between SPI3 enable (step 22) and the handshake (step 30). The stock firmware uses SysTick-based delays with values loaded from RAM. Skipping these delays can cause the handshake to fail silently.
No. All 15 buttons are on MCU GPIO pins (GPIOB and GPIOC). They use a 3-group multiplexed scanning scheme in input_and_housekeeping (0x08039188). The scanning is driven by TMR3. If TMR3 is not running, buttons will not be read.
Button debounce uses 70-tick (short press) and 72-tick (long press) thresholds with the button map table at 0x08046528.
It must be TMR3-driven. The stock firmware does not use a free-running USART with interrupt-driven TX/RX. Instead, TMR3 fires periodically and the ISR calls the USART handler, which pumps one TX byte per invocation and collects RX bytes. Attempting to use standard interrupt-driven USART or polled USART will fail because the FPGA expects specific inter-byte timing.
Also: after each TX frame, there must be a 10ms delay (vTaskDelay(10)) before the next frame.
The IWDG has a ~5 second timeout. It is fed by input_and_housekeeping every 11 calls (approximately every 50ms). If you disable TMR3 for debugging or enter a long loop, the watchdog will fire.
// Feed the watchdog
IWDG_KR = 0xAAAA; // IWDG_RELOAD_KEY at 0x40003000Check that:
- The meter semaphore (
0x20002D7C) is being posted by the USART RX handler when a complete 0x5A 0xA5 frame arrives - BCD extraction uses cross-byte nibble pairs, not byte-aligned nibbles
- The double-precision calibration path is using the correct EABI soft-float functions
ms[0xF35](meter data valid flag) is set
The ADC zero offset of -28.0 is critical and hardware-specific to this FPGA. Without it, all waveform positions will be shifted by ~28 LSBs (~11% of full scale). This constant is hardcoded in VFP register s16 and must be applied to every raw sample before any other calibration.
Before expecting SPI3 data:
- AFIO remap done:
AFIO_PCF0 = (AFIO_PCF0 & ~0xF000) | 0x2000 - SPI3 clock enabled:
RCU_APB1EN |= 0x8000 - SPI3 configured: Mode 3, Master, /2, 8-bit, MSB first, SW NSS
- SPI3 enabled:
SPI3_CTL0 |= 0x40 - PC6 set HIGH:
GPIOC_BOP = (1 << 6) - PB11 set HIGH:
GPIOB_BOP = 0x800 - USART2 configured: 9600 baud, 8N1
- Boot commands sent via USART2: 0x01, 0x02, 0x06, 0x07, 0x08
- SysTick delays executed between boot phases
- SPI3 handshake (command 0x05) completed
- TMR3 enabled and driving USART exchange
- IWDG initialized and being fed every ~50ms
- Acquisition trigger: send items to
spi3_data_queuefrom timer callback - Double-buffer: send TWO queue items per acquisition trigger
- ADC offset: subtract 28.0 from every raw sample
| Address | Name | Item Size | Depth | Purpose |
|---|---|---|---|---|
0x20002D6C |
usart_cmd_queue |
1 byte | 20 | USART command dispatch |
0x20002D70 |
button_event_queue |
1 byte | 15 | Button events from input scanning |
0x20002D74 |
usart_tx_queue |
2 bytes | 10 | Commands to send to FPGA via USART |
0x20002D78 |
spi3_data_queue |
1 byte | 15 | Triggers for SPI3 acquisition |
0x20002D7C |
meter_semaphore |
0 (binary) | 1 | Signals meter RX frame complete |
0x20002D80 |
fpga_semaphore_1 |
0 (binary) | 1 | SPI3 init synchronization |
0x20002D84 |
fpga_semaphore_2 |
0 (binary) | 1 | EXTI notification |
Key fields at offsets from meter_state base (0x200000F8):
| Offset | Type | Name | Description |
|---|---|---|---|
+0x04 |
int8 | ch1_dc_offset |
Channel 1 DC offset (signed) |
+0x05 |
int8 | ch2_dc_offset |
Channel 2 DC offset (signed) |
+0x10 |
uint8 | meter_sub_mode |
Meter sub-mode (0-7) for display format |
+0x14 |
uint8 | spi3_transfer_mode |
0=idle/dummy, nonzero=active |
+0x16 |
uint8 | active_channel |
Active channel (0 or 1) |
+0x17 |
uint8 | hold_mode |
0=running, 1=single, 2=hold |
+0x18 |
uint8 | trigger_mode |
Trigger configuration |
+0x1A |
int16 | trigger_offset |
Signed trigger position offset |
+0x1C |
int16 | voltage_range |
Voltage range encoding (signed) |
+0x2D |
uint8 | timebase_index |
Timebase setting (0x00-0x13) |
+0x32 |
uint8 | acquisition_timer |
Acquisition counter (0xFF=disabled) |
+0x33 |
uint8 | calibration_mode |
0=normal, nonzero=calibrating |
+0x46 |
uint16 | spi3_status_word |
SPI3 16-bit status readback |
+0x352 |
uint8 | ch1_probe_type |
CH1 probe (0/0xFF=none) |
+0x353 |
uint8 | ch2_probe_type |
CH2 probe (0/0xFF=none) |
+0x356 |
uint8[301] | roll_buf_ch1 |
Roll mode circular buffer CH1 |
+0x483 |
uint8[301] | roll_buf_ch2 |
Roll mode circular buffer CH2 |
+0x5B0 |
uint8[1024] | adc_buf_ch1 |
ADC sample buffer CH1 |
+0x9B0 |
uint8[1024] | adc_buf_ch2 |
ADC sample buffer CH2 |
+0xDB0 |
uint8 | spi3_frame_count |
SPI3 frame counter |
+0xDB4 |
uint16 | roll_read_ptr |
Roll mode read pointer |
+0xDB6 |
uint16 | roll_sample_count |
Roll mode count (max 300) |
+0xDB8 |
uint16 | acq_sample_count |
Acquisition sample counter |
+0xF2D |
uint8 | meter_mode_state |
Meter mode (0-8) |
+0xF30 |
uint32 | meter_raw_value |
Raw meter measurement |
+0xF34 |
uint8 | meter_result_class |
1=normal, 2=under, 3=over, 4=invalid |
+0xF35 |
uint8 | meter_data_valid |
Meter data validity flag |
+0xF36 |
uint8 | meter_overload_flag |
Overload state |
+0xF37 |
uint8 | display_decimal_shift (DAT_2000102f) |
Display decimal/formatter state, not factory calibration |
+0xF39 |
uint8 | auto_range_flag |
Auto-range enable |
+0xF3A |
uint16 | meter_extra_data |
Extra from RX bytes [10:11] |
+0xF3C |
uint16 | exchange_lock |
USART exchange lock (0=unlocked) |
+0xF5D |
uint8 | continuity_buzzer |
0xB0=off, 0xB1=on |
+0xF68 |
uint8 | mode_state |
System mode (1=active, 2-9=specific) |
| Pin | Function | Direction | Config | Set During |
|---|---|---|---|---|
| PA2 | USART2 TX | Output | AF push-pull | Phase 2 (step 9) |
| PA3 | USART2 RX | Input | Floating | Phase 2 (step 9) |
| PB3 | SPI3 SCK | Output | AF push-pull 50MHz | Phase 5 (step 18) |
| PB4 | SPI3 MISO | Input | Floating | Phase 5 (step 18) |
| PB5 | SPI3 MOSI | Output | AF push-pull 50MHz | Phase 5 (step 18) |
| PB6 | SPI3 CS | Output | GPIO push-pull | Phase 5 (step 18) |
| PB11 | FPGA active | Output | GPIO push-pull | Phase 8 (step 52) |
| PC6 | FPGA SPI enable | Output | GPIO push-pull | Phase 5 (step 19) |
| PC7 | Probe detect | Input | -- | Phase 1 |
| PC9 | Power hold | Output | GPIO push-pull | Phase 1 (step 2) |
This document was synthesized from the following reverse engineering artifacts:
| File | Contents |
|---|---|
analysis_v120/FPGA_TASK_ANALYSIS.md |
SPI3 format, command table, 9-mode state machine, VFP calibration |
analysis_v120/fpga_task_annotated.c |
580+ lines annotated C code, 10 sub-functions |
analysis_v120/FPGA_PROTOCOL.md |
USART frame format, mode dispatcher, command map |
analysis_v120/FPGA_BOOT_SEQUENCE.md |
53-step boot timeline, GPIO pin map |
analysis_v120/gap_functions.md |
16 new FPGA command codes from gap function analysis |
COVERAGE.md |
Subsystem coverage tracker, function inventory |