From c7ce4f512cd53d59b1565191c2702449f86f56a2 Mon Sep 17 00:00:00 2001 From: Rishab Date: Thu, 17 Sep 2026 09:03:51 -0400 Subject: [PATCH 01/10] Implemented initial RS485 integration for Toad EC. Fixed ec_main.cpp merge conflict --- firmware/CODEBASE_GUIDE.md | 754 +++++++++++++++++++++++++++++++++++ firmware/lib/RS485/RS485.cpp | 170 ++++++++ firmware/lib/RS485/RS485.h | 69 ++++ firmware/src/ec_main.cpp | 9 +- 4 files changed, 997 insertions(+), 5 deletions(-) create mode 100644 firmware/CODEBASE_GUIDE.md create mode 100644 firmware/lib/RS485/RS485.cpp create mode 100644 firmware/lib/RS485/RS485.h diff --git a/firmware/CODEBASE_GUIDE.md b/firmware/CODEBASE_GUIDE.md new file mode 100644 index 0000000..4fde535 --- /dev/null +++ b/firmware/CODEBASE_GUIDE.md @@ -0,0 +1,754 @@ +# TOAD Firmware — Codebase Guide + +A from-scratch orientation to `firmware/` for engineers who are new to this repo. Scope: embedded firmware only (`firmware/`) — the UI directory is intentionally excluded. This is a snapshot as of commit `4a25a3b` on branch `features/RS485`; treat any "currently a stub" / "TODO" note as something that may have changed since — check the file before relying on it. + +--- + +## 1. The big picture + +This is **one PlatformIO project that builds three separate firmware images** from a shared `src/` and `lib/` tree: + +| PlatformIO env | Board | Compiles | Role | +|---|---|---|---| +| `engine_controller` | `TOAD_H7` | `src/ec_main.cpp` (+ any `ec_*.cpp`) | **Engine Controller (EC)** — the main, most-developed firmware. Reads pressure/temperature sensors, drives throttle valves, TVC actuators, RCS thrusters, and solenoid/ball valves. Runs the flight control loop. | +| `flight_controller` | `TOAD_H7` (same board type as EC "for now") | `src/fc_main.cpp` (+ any `fc_*.cpp`) | **Flight Controller (FC)** — currently a stub, not yet implemented. | +| `programmer` | `TOAD_G4` (different, smaller MCU) | `src/prog_main.cpp` (+ any `prog_*.cpp`) | **Programmer board** — a CAN-to-SPI bridge used to remotely flash new firmware onto an EC or FC H7 board over the GSE CAN bus, without a physical debugger. | + +This selection happens via `build_src_filter` in [`platformio.ini`](platformio.ini) — each env excludes all `.cpp` files then re-includes only the ones matching its prefix. `lib_ldf_mode = chain+` means PlatformIO only pulls in a `lib/*` folder if something in the active build actually `#include`s it, so each env only compiles the libraries it needs. + +**If you're new, start here, in this order:** +1. [`platformio.ini`](platformio.ini) — see what actually gets built. +2. [`lib/hardware_mapping/ec_pins.h`](lib/hardware_mapping/ec_pins.h) — the map of "what's plugged into which pin." +3. [`src/ec_main.cpp`](src/ec_main.cpp) — the entry point that wires everything together. +4. Then dip into whichever `lib/` subsystem you're touching. + +--- + +## 2. `platformio.ini` + +Single manifest. A shared `[env]` base plus the three environments above. + +```ini +[env] +platform = ststm32 @ 20.0.0 +framework = arduino +build_flags = + -D SERIAL_RX_BUFFER_SIZE=1024 ; increase serial buffer size + -D SERIAL_TX_BUFFER_SIZE=1024 + -Wl,-u,_printf_float + -Wl,-u,_scanf_float + -D USBCON + -D USBD_USE_CDC + -D RADIO_BAUD=57600 ; used on all Serial interfaces for consistency, see monitor_speed above + -O3 ; compile for speed +``` + +- `platform = ststm32 @ 20.0.0` is **pinned deliberately** — see [§9, the `Uart`/`HardwareSerial` story](#9-where-is-hardwareserial--uart-defined) for why. +- `RADIO_BAUD=57600` is the one baud rate used consistently across every UART in the firmware. +- Each env adds its own define (`TOAD_ENGINE_CONTROLLER_ONLY`, `TOAD_FLIGHT_CONTROLLER_ONLY`, `TOAD_PROGRAMMER_ONLY`) and its own `build_src_filter`. +- The `flight_controller` env has a TODO: `; TODO - add DFU support, and "upload over CAN" if we are feeling clever.` + +--- + +## 3. `src/` — the three entry points + +### `ec_main.cpp` — Engine Controller (the main firmware) + +This is the file that owns and wires together nearly every subsystem in `lib/`. Read this file first for the real picture; everything below is a map of what it touches. + +**Global hardware objects declared here** (and `extern`-referenced from `ec_pins.h`): + +```cpp +CommsSerial_t USB_CommsSerial; +CommsSerial_t HW_CommsSerial(PIN_HW_COMM_SERIAL_RX, PIN_HW_COMM_SERIAL_TX); +CommsSerial_t HW_FallbackSerial(PIN_HW_FALLBACK_SERIAL_RX, PIN_HW_FALLBACK_SERIAL_TX); +// TODO - configure DE pin +Uart RS485_6(PIN_RS485_6_RX, PIN_RS485_6_TX, PIN_RS485_6_DE); +Uart RS485_2(PIN_RS485_2_RX, PIN_RS485_2_TX, PIN_RS485_2_DE); + +SPIClass PT_TC_SPI_1(PIN_PT_TC_SPI_1_MOSI, PIN_PT_TC_SPI_1_MISO, PIN_PT_TC_SPI_1_SCK); +SPIClass PT_TC_SPI_3(PIN_PT_TC_SPI_3_MOSI, PIN_PT_TC_SPI_3_MISO, PIN_PT_TC_SPI_3_SCK); +``` + +Note the live `// TODO - configure DE pin` sitting right above the RS485 UART construction — RS485 direction control via the 3rd constructor arg isn't verified yet. + +**Control flow — this is not a plain Arduino sketch.** `loop()` is deliberately minimal: + +```cpp +void loop() { + while (CommsSerial.available()) { + CommandRouter::receive_byte(CommsSerial.read()); + } +} +``` + +It just pumps bytes from the primary serial into the command router. The real flight logic lives in a separate **`flight_loop()`** function that is registered as a CLI command (`CommandRouter::add(flight_loop, "start_flight_loop")`) and only executes once an operator types that command over serial. `flight_loop()` is a blocking `while(true)`: + +1. Resets `kill_flag`/`arm_flag`, drives throttle valves to a 30° starting angle, zeroes TVC, closes RCS. +2. Loop body: drains any pending serial commands (so `kill_flag` can be set mid-flight via a `"k"` command), reads PT/TC sensors, calls `ValveController::get_controller_output()`, and — **only if `arm_flag` is set** — actually commands the throttle valves, TVC actuators, and RCS. +3. On `kill_flag`, breaks out and safes the throttle valves + RCS. + +Two `CommandRouter::add_flag` calls wire the CLI commands `"k"` and `"arm"` to `kill_flag`/`arm_flag`, so an operator can arm or abort the flight loop live. + +`setup()`: starts all comm serials at `RADIO_BAUD`, RS485 buses at 9600 (`// TODO - what baud?`), SPI buses, waits 3s, prints a startup banner to both `CommsSerial` (macro, see §9 CommsSerial section) and the fallback serial, then calls every subsystem's `begin()` and ANDs the results into `all_modules_ok`. **If any module fails init, the firmware halts in an infinite loop printing an error every 5s rather than proceeding** — a deliberate fail-safe. + +Depends on: `CommandRouter.h`, `CommsSerial.h`, `PressureSensors.h`, `RCS.h`, `SolenoidValves.h`, `TVC_Actuators.h`, `TemperatureSensors.h`, `ThrottleValves.h`, `ValveController.h` (which transitively pull in `ec_pins.h`, `ec_sensors.h`, `ec_valves.h`, `toad_can_bus.h`). + +### `fc_main.cpp` — Flight Controller (stub, not yet implemented) + +```cpp +void setup() +{ + Serial.begin(9600); + Serial.println("init"); // had to add this; for some reason without it we don't pull in Print.cpp which causes a linker error because _write is not defined +} +void loop() {} +``` + +That comment documents a real STM32duino linker quirk: without at least one `Serial.println()` call, `Print.cpp` never gets linked in, and `_write` (used by libc stdio) ends up undefined, breaking the link. The `#include "CommsSerial.h"` at the top is commented out — FC doesn't use the shared comms layer yet. + +### `prog_main.cpp` — Programmer board (CAN-to-SPI bootloader bridge) + +Sits between the GSE CAN bus and an STM32H7 target (EC or FC board) and can force that H7 into its ST SPI bootloader to flash it without a debugger or USB cable. Explicitly implements ST's app note **AN4286** ("How to use SPI protocol in bootloader on STM32 MCUs") — comments are literally annotated with page citations, e.g. `// Send SYNC byte and get ACK [6]`, `// Mass erase [26]`. + +Key protocol constants: +```cpp +constexpr uint8_t ACK = 0x79; +constexpr uint8_t NACK = 0x15; +constexpr uint8_t SYNC = 0x5A; +constexpr uint8_t CMD_WriteMem = 0x31; +constexpr uint8_t CMD_EraseMem = 0x44; +``` + +Flash paging: +```cpp +// 2 MB of Flash divided into 512 x 4096 byte virtual pages. +// Chunks for the active page are received 32 bytes at a time, for 128 chunks per page. +// The active page is written out 256 bytes at a time. +constexpr size_t CAN_CHUNK_SIZE = 32; +constexpr size_t WRITE_CHUNK_SIZE = 256; +constexpr size_t PAGE_CACHE_SIZE = 4096; +constexpr size_t NUM_PAGE_CACHES = 512; +static_assert(PAGE_CACHE_SIZE * NUM_PAGE_CACHES == 2097152); +``` + +State machine: `enum prog_state_t { STATE_IDLE, STATE_PRE_ERASE, STATE_READY, STATE_PAGE_SELECTED }`. + +One physical programmer image serves **both** EC and FC boards — at boot it reads a GPIO strap pin to decide which target it's talking to: `prog_type = (digitalRead(PIN_PROG_ID) == PROG_ID_FLIGHT_CONTROLLER) ? PROG_FLIGHT_CONTROLLER : PROG_ENGINE_CONTROLLER;` + +Key functions: `reset_h7()` (pulses NRST), `spi_ack_frame()` (flagged `// TODO - pretty sure sometimes this has to cycle around until ACK`), `enter_bootloader()`, `erase_memory()`, `write_memory(addr, bytes, len)`. + +`loop()` is a CAN-message-driven state machine built on the `CAN_Msg_Decoder` template from `toad_can_bus.h` (see §5). **Known incomplete/buggy areas**: `raw_bytes`/`raw_msg_len` are declared but never actually populated from the CAN peripheral (`// TODO - figure out how to parse the incoming cmd and payload` is left unresolved), and there is a likely real bug where `if (!chunk_rcv)` (an array, always truthy as a pointer) should probably be `if (!chunk_rcv[i])`. + +--- + +## 4. `boards/` — board variant packages (mostly vendor boilerplate) + +`boards/TOAD_G4/` and `boards/TOAD_H7/` are STM32duino Arduino-core "variant" packages — largely auto-generated by ST's CubeMX tooling (BSD-3-Clause, `Copyright (c) 2020, STMicroelectronics`, banner comment `Automatically generated from STM32G473R(B-C-E)Tx.xml ... CubeMX DB release 6.0.160`). You generally won't need to edit these, but it helps to know what's in them: + +- **`PinNamesVar.h`** — alternate-function pin aliases, wakeup pin mappings, USB D+/D- assignments. +- **`variant_TOAD_*.h`** — numeric pin `#define`s, pin counts, default `LED_BUILTIN`, default SPI/I2C/UART pins, `SERIAL_PORT_*` aliasing macros. `variant_TOAD_H7.h` has one project-specific addition at the end: `// Robert additions` → `#define USE_PWR_LDO_SUPPLY`. +- **`variant_TOAD_*.cpp`** — pin lookup tables plus `SystemClock_Config()`. G4 uses HSI+HSI48 oscillators for USB; H7 uses a separate PLL3 tuned specifically for 48 MHz USB (`PLL3M=32, PLL3N=192, PLL3Q=8`) plus `HAL_PWREx_ConfigSupply(PWR_LDO_SUPPLY)`. +- **`PeripheralPins.c`** — ST/CubeMX-generated pin↔peripheral capability tables (ADC, I2C, TIM, UART, SPI, FDCAN, USB, etc.). +- **`ldscript.ld`** — standard linker script; heap/stack sizes reserved via `_Min_Heap_Size`/`_Min_Stack_Size`. + +Practical difference between the boards: G4 defaults to `SERIAL_UART_INSTANCE 2`, H7 to `5`; H7 needs the extra `USE_PWR_LDO_SUPPLY`/PLL3-for-USB tweak that G4 doesn't. + +--- + +## 5. `lib/can_bus/toad_can_bus.h` — CAN topology and message formats + +Defines the CAN network topology, 11-bit CAN IDs, and every custom CAN wire-format message struct. + +Topology, verbatim: +``` +// The primary vehicle CAN bus runs from the flight +// controller to the engine controller and includes +// the power management board. This is a CAN-FD bus. + +// The engine control CAN bus runs from the engine +// controller to the TVC actuators and the stepper +// drivers. This is a CAN 2.0 bus. + +// The GSE CAN bus runs from the GSE, over the QD +// arm and splits to run to each programmer board. +// This is a CAN-FD bus. +``` + +So there are **three physically distinct CAN networks** — this is why `ec_pins.h` defines both `PIN_CAN_TVC_RX/TX` (engine-control CAN 2.0 bus, to TVC/steppers) and `PIN_CAN_FC_RX/TX` (vehicle CAN-FD bus, to the flight controller). + +CAN IDs: +```cpp +// COTS devices (ID range 0x00X) +constexpr uint16_t CAN_ID_TVC_PITCH = 0x001; +constexpr uint16_t CAN_ID_TVC_YAW = 0x002; +constexpr uint16_t CAN_ID_STEPPER_OX = 0x003; +constexpr uint16_t CAN_ID_STEPPER_FU = 0x004; + +// Custom boards (ID range 0x01X) +constexpr uint16_t CAN_ID_FLIGHT_CONTROLLER = 0x011; +constexpr uint16_t CAN_ID_ENGINE_CONTROLLER = 0x012; +constexpr uint16_t CAN_ID_FLIGHT_PROG = 0x013; +constexpr uint16_t CAN_ID_ENGINE_PROG = 0x014; +constexpr uint16_t CAN_ID_GSE = 0x015; +constexpr uint16_t CAN_ID_POWER_BOARD = 0x016; +``` + +Every message struct starts with `uint8_t cmd_id` as a wire discriminator: `can_msg_heartbeat_t` (0x00), error replies `can_msg_invalid_cmd_t`/`can_msg_incorrect_len_t`/`can_msg_unexpected_state_t` (0x01–0x03), telemetry `can_msg_fc_telemetry`/`can_msg_ec_telemetry` (0x10/0x11), and the bootloader protocol messages `can_msg_reset_controller_t`, `can_msg_enter_bootloader_t`, `can_msg_erase_flash_t`, `can_msg_select_page_t`, `can_msg_mem_packet_t`, `can_msg_request_mem_packet_t`, `can_msg_write_flash_t` (0x30–0x36). + +**The one real class here:** + +```cpp +template class CAN_Msg_Decoder { +public: + CAN_Msg_Decoder(const uint8_t *raw_bytes, size_t len, state_t state); + template std::optional decode_and_enforce_state(state_t expected); + template std::optional decode(); // convenience: expected = state + void send_error_if_not_decoded(); +private: + bool decoded; + const uint8_t *raw_bytes; + const size_t len; + const state_t state; +}; +``` + +Usage pattern (as seen in `prog_main.cpp`): +```cpp +if (const auto msg = raw_msg.decode()) { ... } +else if (const auto msg = raw_msg.decode()) { ... } +``` + +`decode_and_enforce_state()` matches `raw_bytes[0]` against `msg_t`'s cmd_id, validates payload length equals `sizeof(msg_t)`, validates the state machine is in the expected state, and on success `memcpy`s into the typed struct. + +**Important gap**: none of the error-struct "send" TODOs are implemented anywhere — there is currently **no actual CAN transmit path** wired up in this decoder or its call sites. It prepares error structs locally and drops them. + +--- + +## 6. `lib/hardware_mapping/` — "what's plugged into which pin" + +This is the authoritative, human-curated source of pin assignments, separate from the low-level Arduino pin numbering in `boards/`. + +### `ec_pins.h` + +Organized by peripheral group, each with explanatory comments: +- **UARTs**: `PIN_HW_COMM_SERIAL_RX/TX` = primary serial (UART5), `PIN_HW_FALLBACK_SERIAL_RX/TX` = fallback (UART3). +- **RS485**: `// RS485 Busses (UART6 and UART2)`; `extern Uart RS485_6; extern Uart RS485_2;` (owned by `ec_main.cpp`), plus per-device bus/select macros: `TVC_PITCH_RS485_BUS`/`PIN_TVC_PITCH_SEL`, `ENC_OX_RS485_BUS`/`PIN_ENC_OX_SEL`, yaw/fuel equivalents on `RS485_2`. `DRV_OX_RS485_BUS`/`PIN_DRV_OX_SEL` marked `// unused`. +- **SPI**: two buses (`PT_TC_SPI_1`, `PT_TC_SPI_3`) shared by the PT and TC boards. +- **QSPI**: pins defined, not obviously used yet. +- **FDCAN**: `PIN_CAN_TVC_RX/TX`, `PIN_CAN_FC_RX/TX`. +- **PWM (spark/igniter)**: `PIN_SPARK_PWM`/`PIN_SPARK_TRIG`. +- **"Zucrow Board" pins**: `// TODO - replace with PT board definition` — placeholder for a separate DI/DO interface board (named for Purdue's Zucrow Labs test stand). +- **Valve DOs**: `NUM_SV_BV_VALVES 16`, `PIN_SV_DO_1..9`, `PIN_BV_DO_10..16`, `PIN_VALVE_OE_INPUT`, `PIN_SV_BV_LATCH_ENABLE` (`// TODO - flywire and assign`). +- **PT/TC boards**: `NUM_PT_BOARDS 6` (each = 1 `ADS131M02` driving 2 PT channels), `NUM_TC_CHIPS 6`, split across the two SPI buses. +- **Utility macros**: `CONCAT`/`STRINGIFY` (classic preprocessor token-paste/stringize helpers) used heavily by `ec_sensors.h`/`ec_valves.h` to build canonical names. + +### `ec_sensors.h` + +Maps numeric PT/TC indices to canonical P&ID-style names, e.g.: +```cpp +#define PT_1 PT_N2_01_tank +#define PT_2 PT_N2_02_reg +... +#define PT_12 PT_FU_05_venturi_upstream +static_assert(NUM_PT_BOARDS == 6); + +#define TC_1 TC_N2_01_tank +... +static_assert(NUM_TC_CHIPS == 6); +``` + +Shared reading structs: +```cpp +struct pressure_readings_t { uint8_t crc_errors; float PT_1..PT_12; }; // all readings in PSI +struct temperature_readings_t { float TC_1..TC_6; }; // all readings in K +``` + +`PT_CALIBRATION(n)` macro expands to e.g. `PT_1_calibration`, defined in `pt_calibration.h`. + +### `ec_valves.h` + +Canonical naming + default fail-safe states for all 16 valve DOs: +```cpp +#define VALVE_SHORT_NAME_LEN 8 // "SV_N2_01" +#define SV_1 SV_N2_01_rcs_pos_1 +... +#define BV_16 BV_FU_03_run +static_assert(NUM_SV_BV_VALVES == 16); + +namespace SolenoidValves { + enum valve_ids { ... }; + enum valve_state_t { VALVE_CLOSE, VALVE_OPEN }; + // A valve state isn't the same as a DO logic level, so use this type to differentiate. + // See valve_state_to_logic_level for more info. +} +``` + +Default positions: N2/O2/FU "release" ball valves default **open** (vent-safe); everything else (RCS, purges, igniters, "run" valves) defaults **closed** (marked `// TODO CONOPS - audit`). Comment in this file: `// Note - this must be kept in sync with the list in SolenoidValves.cpp`. + +### `prog_pins.h` + +Programmer-board (`TOAD_G4`) pins and the boot-control protocol used to bridge to the H7 target: +```cpp +#define PIN_PROG_ID PB1 +#define PROG_ID_FLIGHT_CONTROLLER HIGH +#define PROG_ID_ENGINE_CONTROLLER LOW +enum prog_id_t { PROG_FLIGHT_CONTROLLER, PROG_ENGINE_CONTROLLER }; + +#define PIN_H7_BOOT PC0 +#define BOOT_MODE_RUN LOW +#define BOOT_MODE_FLASH HIGH + +#define PIN_H7_NRST PC1 +#define NRST_MODE_RUN HIGH +#define NRST_MODE_RST LOW +``` +Same firmware image serves both EC and FC — differentiated only by the `PIN_PROG_ID` strap — and directly drives the target H7's BOOT0/NRST pins. + +--- + +## 7. `lib/pressure_sensors/` + +### `ADS131M02.h` / `.cpp` — low-level SPI driver + +TI ADS131M02, a 2-channel 24-bit delta-sigma ADC used to read pressure transducers. (`// Datasheet: https://www.ti.com/lit/ds/symlink/ads131m02.pdf`, citations marked `[pg#]`.) + +```cpp +struct adc_reading_t { uint32_t status_reg; int32_t ch0; int32_t ch1; bool crc_ok; }; + +class ADS131M02 { +public: + ADS131M02(SPIClass spi_bus, unsigned int cs_pin); + void begin(); + adc_reading_t read_adc(); +private: + uint32_t transact_word(uint32_t cmd, uint8_t *crc_buf); + SPIClass spi_bus; + unsigned int cs_pin; +}; +``` + +`SPISettings ADS131M02_SPI_SETTINGS(4000000, MSBFIRST, SPI_MODE1);` with `// TODO - verify clock integrity with a scope.` `read_adc()` sends the ADC's "NULL" command 4 times (status/ch0/ch1/CRC words per frame), computes a CRC-16 (poly `0x1021`) to validate, and sign-extends the 24-bit two's-complement channel values. Frame protocol comment: `// The ADS131M02 communicates in 24 bit words, grouped into 4 word frames ... This function should be called 4 times to process a complete frame.` + +### `PressureSensors.h` / `.cpp` — application layer + +```cpp +class PT_Board { +public: + PT_Board(SPIClass &spi_bus, unsigned int cs_pin, float pt0_slope, float pt0_offset, float pt1_slope, float pt1_offset); + void begin(); + bool read_pts(float *pt0_reading, float *pt1_reading); +private: + ADS131M02 adc; + float pt0_slope, pt0_offset, pt1_slope, pt1_offset; +}; + +namespace PressureSensors { + bool begin(); + pressure_readings_t read_pts(); + void print_pt_crc_errors(pressure_readings_t pt_readings); + void print_pt_readings(); +} +``` + +Instantiates all 6 `PT_Board`s using the `PT_CALIBRATION(n)` macro for slope/offset. `read_pts()` has a live TODO: `// TODO PTs - add preconversion logic to account for voltage level changes / probably need to divide by pow(2, 24) as well` — today's raw-code→engineering-units conversion is a naive linear `slope*raw+offset` and is flagged as incomplete. `begin()` does an initial read; if CRC errors are found at boot it prints them and **returns false**, which propagates into `ec_main.cpp`'s `all_modules_ok` fail-safe halt. Registers CLI command `print_pt`. + +### `pt_calibration.h` + +Per-sensor linear calibration constants — **currently all placeholders**: every PT is `slope=0, offset=10`. Don't trust any PT reading until these are filled in with real calibration data. + +--- + +## 8. `lib/RS485/` — new, untracked, effectively empty (relevant to your current branch) + +This is the directory shown as untracked in `git status` on `features/RS485`. + +```cpp +// RS485.h +#pragma once +#include +#include +#include + +class RS485{ + +} +``` + +This is a stub: the class body is empty, and **the class definition is missing its terminating semicolon** — `class RS485{ ... }` with no `;` — which will fail to compile if this header is ever `#include`d and the type used. `RS485.cpp` is completely empty (0 bytes). + +Nothing else in the codebase references this file yet. Today, RS485 buses are implemented directly as plain `Uart` objects with a manually-toggled `SEL`/DE pin (see `ec_main.cpp`'s `RS485_6`/`RS485_2`, and `AMT242AV`'s hand-rolled RS485 bit-banging below) — this library looks like the start of an effort to factor that pattern out into a reusable class, but it isn't wired into anything yet. + +--- + +## 9. `lib/serial_comms/` + +### `CommsSerial.h` + +Template wrapper adding buffered `readline()`, `scanf`-style parsing, and `printf`-style convenience methods on top of any Arduino serial-like class (`Uart`, `USBSerial`). Also declares the two shared comm-serial globals used everywhere. + +```cpp +#define PRINT_BUFFER_SIZE 1024 +#define READ_BUFFER_SIZE 1024 + +template class CommsSerial_t : public BaseSerial { +public: + using BaseSerial::BaseSerial; + char readbuf[READ_BUFFER_SIZE]; + char *readline(); + template int scanf(const char *format, Args... args); + template void printf(const char *format, Args... args); + template void mprint(const T &t); + template void mprint(const T &t, const Args &...args); + template void mprintln(const Args &...args); +}; + +extern CommsSerial_t HW_CommsSerial; +extern CommsSerial_t USB_CommsSerial; + +#define CommsSerial HW_CommsSerial +``` + +**Important gotcha**: `CommsSerial` is a **macro**, not a variable — `#define CommsSerial HW_CommsSerial`. This is why code all over the tree (`PressureSensors.cpp`, `SolenoidValves.cpp`, etc.) can just write `CommsSerial.println(...)` and have it transparently mean "the primary hardware comm UART." Because it's a preprocessor macro, it can't be locally shadowed or reassigned — keep that in mind if you ever want a function-local variable named `CommsSerial`. + +`readline()` busy-waits reading characters until `\n`, with `\b` as a destructive backspace. `printf` snprintfs into a 1 KB stack buffer, then calls `BaseSerial::print()`. + +### `CommandRouter.h` / `.cpp` + +A newline-delimited ASCII command router over `CommsSerial`, with escaped control characters, used both as an interactive CLI for GSE/test operators and as the registration point for firmware modules' own commands (`print_pt`, `open_valve`, `start_flight_loop`, etc.) and flags (`k`, `arm`). + +```cpp +struct command { + std::function f; + const char *name; + const char *help; +}; + +#define END_CHAR '\n' +#define CR_CHAR '\r' +#define ESCAPE_CHAR '\\' +#define BACKSPACE_CHAR '\b' + +namespace CommandRouter { + void begin(); + void receive_byte(uint8_t c); + template void send_command(const char *command, S data); + void help(const char *cmd_name); + void add(std::function f, const char *name, const char *help = "no help provided"); + void add(std::function fstr, const char *name, const char *help = "no help provided"); + void add(std::function fvoid, const char *name, const char *help = "no help provided"); + void add_flag(bool *flag, const char *name, const char *help); +} +``` + +`receive_byte()` is a byte-at-a-time state machine (fed from `ec_main.cpp`'s `loop()`) that builds a command buffer (max `MAX_CMD_LEN 1024`), handles `\n` (dispatch), `\r` (ignored — `// do nothing - we aren't a typewriter, no need to carriage return`), `\b` (backspace), and `\` (escape, so binary payloads like `sv_ui`'s 4-byte bitmask can embed the 4 special bytes literally). `help()` supports `help` (list all), `help `, and `help ` (prefix match — `// no command found, so print all commands that start with what user entered`). `send_command()` is the outbound side: writes a command name plus a raw binary payload, escaping special bytes, for sending structured telemetry to a host. Built-in commands: `help`, and `ping` (`CommsSerial.println("pong")`, connectivity check). + +--- + +## 10. `lib/solenoid_valves/` + +### `SolenoidValves.h` / `.cpp` + +Drives the 16 solenoid/ball-valve digital outputs through what's implied to be a latching relay driver (based on the `pulse_latch_enable()` pattern), converting between abstract `VALVE_OPEN`/`VALVE_CLOSE` and the correct GPIO level per valve. + +```cpp +namespace SolenoidValves { + bool begin(); + void pulse_latch_enable(); + void set_valves_from_valve_state(uint32_t valve_states); + void set_valves_from_valve_state_cmd(const uint8_t *cmd_packet, size_t len); + void set_valve_by_num(int i, valve_state_t state, bool pulse_latch = true); + void open_valve_by_name(const char *name); + void close_valve_by_name(const char *name); +} +``` + +**Key safety design note — read this if you touch valve code.** Verbatim: +```cpp +// Converts from VALVE_OPEN / VALVE_CLOSE to LOW / HIGH depending on the valve wiring. +// All valves must enter a 'default / safe' state when they receive a LOW signal. +// This requirement is driven by the flight termination system functionality. +// So to check whether the DO should be LOW or HIGH, just compare against this default state. +bool valve_state_to_logic_level(int valve_num, valve_state_t target_state) { + return target_state == sv_and_bvs[valve_num].default_state ? LOW : HIGH; +} +``` +**LOW always means safe**, regardless of whether "safe" happens to be open or closed for a given valve. A wire break, power loss, or FTS (Flight Termination System) trigger naturally drives every valve pin LOW and lands it in its safe configuration. + +`begin()`: leaves `PIN_SV_BV_LATCH_ENABLE` LOW at boot (`// On boot, leave all valves in the state they were left in by setting latch enable low.`), configures `PIN_VALVE_OE_INPUT` as input (`// TODO - check state of this pin to see if flight has been terminated`), sets every valve pin LOW+OUTPUT, registers CLI commands `open_valve`, `close_valve`, `sv_ui` (binary bitmask command). + +`pulse_latch_enable()` (`// TODO - test this delay / check datasheet`) pulses latch-enable HIGH for 50µs then LOW — this is what actually propagates buffered DO states out to the physical valve driver hardware, implying valve outputs go through a latching driver IC rather than direct GPIO. + +### `RCS.h` / `.cpp` — Reaction Control System + +Simple bang-bang/deadband control of 4 N2 attitude-thruster solenoid valves. + +```cpp +#define RCS_DEADBAND 1 // N? + +namespace RCS { + void close(); + void update_rcs_valves(float rcs_force); +} +``` + +`update_rcs_valves(rcs_force)`: `>= RCS_DEADBAND` opens "pos" valves + closes "neg"; `<= -RCS_DEADBAND` the reverse; otherwise closes all. Each branch sets 4 valve DOs individually with `pulse_latch=false` then calls `pulse_latch_enable()` once, batching the latch pulse. + +--- + +## 11. `lib/temperature_sensors/` + +### `Adafruit_MAX31856.h` / `.cpp` + +SPI driver for the MAX31856 thermocouple amplifier, a trimmed fork of Adafruit's library: `// Modifed by Robert Nies to remove dependency on Adafruit_SPIDevice` / `// We only use Hardware SPI on Toad`. + +```cpp +class Adafruit_MAX31856 { +public: + Adafruit_MAX31856(SPIClass &spi_bus, unsigned int cs_pin, max31856_thermocoupletype_t tc_type); + bool begin(void); + void setConversionMode(max31856_conversion_mode_t mode); + max31856_conversion_mode_t getConversionMode(void); + void setThermocoupleType(max31856_thermocoupletype_t type); + max31856_thermocoupletype_t getThermocoupleType(void); + uint8_t readFault(void); + void triggerOneShot(void); + bool conversionComplete(void); + float readCJTemperature(void); + float readThermocoupleTemperature(void); + void setTempFaultThreshholds(float flow, float fhigh); + void setColdJunctionFaultThreshholds(int8_t low, int8_t high); + void setNoiseFilter(max31856_noise_filter_t noiseFilter); +private: + SPIClass &spi_bus; unsigned int cs_pin; + max31856_conversion_mode_t conversionMode; + max31856_thermocoupletype_t tc_type; + void readRegisterN(uint8_t addr, uint8_t buffer[], uint8_t n); + uint8_t readRegister8(uint8_t addr); + uint16_t readRegister16(uint8_t addr); + uint32_t readRegister24(uint8_t addr); + void writeRegister8(uint8_t addr, uint8_t reg); +}; +``` + +`SPISettings Adafruit_MAX31856_SPI_SETTINGS(4000000, MSBFIRST, SPI_MODE1);` (same unverified-clock TODO as `ADS131M02`). `begin()` enables open-circuit fault detection, zeroes cold-junction offset, sets thermocouple type, enables continuous conversion, and validates connectivity via a register readback of the factory-default `0xC0`. `readThermocoupleTemperature()` reads a 24-bit signed register, sign-extends, shifts off the unused bottom 5 bits, and scales by `0.0078125` (2⁻⁷) to get °C. + +**Bug worth knowing about**: `readRegisterN()` and `writeRegister8()` call `pinMode(cs_pin, LOW)` / `pinMode(cs_pin, HIGH)` to toggle chip-select. This should almost certainly be `digitalWrite()`, not `pinMode()` — likely a copy-paste artifact from the upstream Adafruit source. Whether it actually works depends on this STM32duino `pinMode()` implementation tolerating a non-mode second argument, which is fragile. Worth fixing if you're in this file. + +### `TemperatureSensors.h` / `.cpp` + +```cpp +namespace TemperatureSensors { + bool begin(); + temperature_readings_t read_tcs(); + void print_tc_readings(); +} +``` + +Instantiates all 6 K-type thermocouple channels across the two shared SPI buses. `#define C_TO_KELVIN 273.15`; `read_tcs()` converts to Kelvin (matches `ec_sensors.h`'s `// all readings in K`). + +**Inconsistency worth knowing about**: `print_tc_readings()` (whose own comment says `// print TC readings in Fahrenheit.` and which does call `c_to_f()`) prints using a format string labeled `%6.2f C` — the unit *label* in the printf format is wrong (says C, prints F); the conversion itself is correct. + +--- + +## 12. `lib/throttle_valves/` + +### `AMT242AV.h` / `.cpp` + +Driver for a CUI AMT242A-V absolute magnetic encoder over half-duplex RS485 (`Uart` + a `SEL` GPIO toggling a MAX485 transceiver between TX/RX), used to read throttle-valve shaft position. + +```cpp +class AMT242AV { +public: + AMT242AV(Uart &uart, unsigned int SEL, uint8_t ID); + void begin(); + bool read_pos(float *out, int max_retries = 10); + void zero(); + void reset(); +private: + Uart &uart; unsigned int SEL; uint8_t ID; + bool wait_for_avail(unsigned long long); + bool _read_pos(uint16_t *); +}; +``` + +`#define MAX_READING ((1 << 12) - 1)` — 12-bit encoder resolution. `_read_pos()`: clears stale RX bytes, drives `SEL` HIGH (transmit) with a 70µs settle, sends a single-byte read command (`uart.write(ID)`), waits up to 150µs for a 2-byte response, then decodes 14 bits of `transmission` down to a 12-bit position (`// we are using 12 bit encoder, datasheet says to throw out lowest two bits`), and validates an odd-parity checksum in the top 2 bits (comment: `// highest bit is for odd-numbered bits, second highest is for even / checksums calculated using odd parity`). Always leaves `SEL` LOW (idle/receive) before returning. `read_pos()` retries up to `max_tries`, normalizing the raw reading to `[0.0, 1.0]` (`0.0 -> 0 degrees, ..., 1.0 -> 360 degrees`). + +Top-of-file TODO: `// TODO - audit code below and update for EC, optionally break out RS485 funcs` — plus dead ISR-based code and a comment (`// flush doesn't do anything on portenta h7 but maybe on other platforms it will`) suggesting this was ported from a prior "Portenta" board and hasn't been fully validated on TOAD hardware yet. + +### `MksServo57D.h` / `.cpp` + +CAN driver for a Makerbase MKS SERVO57D closed-loop stepper driver, used to actuate throttle valve motors. Cites the datasheet and a reference implementation directly in comments. + +```cpp +class MksServo57D { +public: + MksServo57D(uint16_t can_id) : can_id(can_id) {}; + void begin(); + void set_speed(int16_t speed, uint8_t acceleration = 32); +private: + template std::array to_be_bytes(T value); + template void send_frame(uint8_t cmd, const std::array &data); + uint16_t can_id; +}; +``` + +`send_frame()` builds a CAN 2.0 frame `[cmd, ...data, crc]` where `crc = (can_id + cmd + sum(data)) & 0xFF`, but **never actually transmits it** — the function ends with `// TODO - transmit the frame` and does nothing further. **This means `set_speed()` currently has no real hardware effect** — it's fully unconnected to any CAN peripheral driver. `set_speed()` itself clamps `|speed|` to `[0, 400]` RPM (`// Max speed in open loop mode is 400 RPM.`) and packs direction into the top bit. + +### `ThrottleValves.h` / `.cpp` + +Combines one `MksServo57D` (motor) + one `AMT242AV` (encoder) per throttle valve (ox and fuel). + +```cpp +class ThrottleValve { +public: + ThrottleValve(uint16_t motor_can_id, Uart &enc_uart, unsigned int enc_SEL, unsigned int enc_ID); + void begin(); + void stop(); + void set_position(float angle); +private: + MksServo57D motor; + AMT242AV encoder; +}; + +namespace ThrottleValves { + bool begin(); + void stop(); + void set_angles_ox_fu(float ox_angle, float fu_angle); +} +``` + +`set_position()` is a bare proportional controller (`// TODO - PID controller logic`): `target_speed = (angle - current_angle) * K` with `float K = 1; // TODO - set this constant` — an unset/untuned P-only gain, no I or D term. Also flagged: `// TODO - if this turns the motor on, make sure we don't leave it on by mistake! could use heartbeat to solve`. **`ThrottleValves::begin()` currently just `// TODO - don't just return true here!`** — no real health check, so unlike `PressureSensors`/`TemperatureSensors`, this module can never fail the `all_modules_ok` gate in `ec_main.cpp`. + +--- + +## 13. `lib/tvc_actuators/` + +### `GimbalKinematics.h` / `.cpp` + +Pure-math library converting a commanded pitch/yaw pair into the two linear actuator extension lengths needed to achieve it, via 3D point rotation. + +```cpp +void calc_actuator_lengths(float primary_angle, float secondary_angle, float *primary_length, float *secondary_length); +``` + +Geometry constants (all marked `// TODO - update these`): +```cpp +#define BASE_POINT_DIST_FROM_ORIGIN_H 6.25 +#define BASE_POINT_DIST_FROM_ORIGIN_V -2 +#define ENGINE_POINT_DIST_FROM_ORIGIN_H 3.475 +#define ENGINE_POINT_DIST_FROM_ORIGIN_V 10.35 +#define BASE_ACTUATOR_LEN 12.6 +``` + +Rotates 4 fixed 3D attachment points by pitch then yaw and computes each actuator's required length as a *delta* from its neutral length (`// output lengths in inches (already subtracted from the base actuator length)`). Standalone and currently unverified — placeholder geometry constants mean computed lengths aren't trustworthy until measured on real hardware. + +### `TVC_Actuators.h` / `.cpp` + +```cpp +namespace TVC_Actuators { + bool begin(); + void set_angles_pitch_yaw(float pitch, float yaw); +} +``` + +`begin()` just `return true;`. `set_angles_pitch_yaw()` calls `calc_actuator_lengths()` but then **does nothing with the result** (`// TODO - this function`) — the actual actuator-commanding logic (presumably over CAN to `CAN_ID_TVC_PITCH`/`CAN_ID_TVC_YAW`) hasn't been written yet. This is the least-implemented module in the tree — a pure stub. + +--- + +## 14. `lib/valve_controller/ValveController.h` / `.cpp` + +Intended home for the EC's closed-loop throttle control algorithm — meant to take live PT/TC readings and compute target throttle-valve angles. Currently a stub. + +```cpp +struct valve_controller_output_t { + float ox_angle; // deg + float fu_angle; // deg +}; + +namespace ValveController { + bool begin(); + valve_controller_output_t get_controller_output(pressure_readings_t pt_readings, temperature_readings_t tc_readings); +} +``` + +`get_controller_output()` (`// TODO - this function`) always returns `{0.0, 0.0}` regardless of input — the core engine-mixture/thrust control loop that `ec_main.cpp`'s `flight_loop()` calls every iteration is entirely a placeholder today. + +--- + +## 15. The sensor/actuator abstraction pattern + +There's **no shared C++ interface/base class** — no virtual `ISensor`/`IValve`. Instead the codebase follows a consistent two-layer, hand-rolled convention repeated per subsystem: + +1. **Low-level driver class** (one per physical chip): `ADS131M02`, `Adafruit_MAX31856`, `AMT242AV`, `MksServo57D`. Each wraps exactly one device, takes its bus/pins in the constructor, exposes `begin()` plus device-specific methods. +2. **Application-level `namespace` singleton** (one per logical subsystem, matching the `ec_*` hardware_mapping headers): `PressureSensors`, `TemperatureSensors`, `SolenoidValves`, `ThrottleValves`, `TVC_Actuators`, `ValveController`. Each owns statically-allocated driver instances for every physical unit, and follows a uniform naming convention: + - `bool begin()` — inits all owned hardware, returns overall success. Consumed by `ec_main.cpp`'s `all_modules_ok &= X::begin();` gate. **Note**: today this gate is only meaningfully enforced by `PressureSensors` and `TemperatureSensors` — `ThrottleValves`, `TVC_Actuators`, and `ValveController` all just `return true`. + - a `read_*()`/`get_*()` returning a plain result struct (`pressure_readings_t`, `temperature_readings_t`, `valve_controller_output_t`) — these structs, defined centrally in `ec_sensors.h`/`ValveController.h`, are the shared data-interchange format between subsystems. + - a `set_*`/action function for actuators (`set_angles_ox_fu`, `set_angles_pitch_yaw`, `update_rcs_valves`, `set_valves_from_valve_state`). + - most register at least one `CommandRouter::add(...)` CLI command for interactive debugging (`print_pt`, `print_tc`, `open_valve`/`close_valve`/`sv_ui`). + +A middle "per-unit" class recurs for multi-channel devices — `PT_Board` (2 PTs sharing 1 ADC) and `ThrottleValve` (1 motor + 1 encoder) — bundling multiple driver instances that logically belong together, instantiated as fixed-size arrays/named globals inside the owning namespace's `.cpp`. + +This keeps each module simple but means some duplication (every SPI sensor driver independently defines its own `SPISettings`, every namespace writes its own `begin()`-failure printf loop) — a candidate for a future shared-interface refactor if you're looking for one. + +--- + +## 16. How CAN, RS485, and Serial fit together + +- **USB / primary serial (`HW_CommsSerial`, aliased `CommsSerial`) and fallback serial (`HW_FallbackSerial`)** — the human/GSE-facing text CLI, driven by `CommandRouter`. Used to arm/kill the flight loop, open/close valves manually, read live sensor values. **Not** part of the real-time flight-critical path — polled non-blockingly once per `loop()`/`flight_loop()` iteration. +- **RS485 (`RS485_6`, `RS485_2`, plain `Uart` + DE pin)** — half-duplex point-to-point links used specifically for the `AMT242AV` absolute encoders (plus reserved-but-unused `DRV_OX_RS485_BUS`/`DRV_FU_RS485_BUS` slots for driver comms). This is the *sensor feedback* bus for throttle-valve position and TVC actuator selection, addressed via per-device `SEL` GPIOs so multiple devices can share one physical bus. +- **CAN (FDCAN peripherals, `toad_can_bus.h`)** — the command/actuation bus. Connects EC to the `MksServo57D` stepper drivers and TVC actuators on the "engine control CAN bus" (CAN 2.0), and separately EC↔FC↔power-board on the "primary vehicle CAN bus" (CAN-FD) and GSE↔programmer-boards on the "GSE CAN bus" (CAN-FD). `CAN_Msg_Decoder` standardizes parsing incoming frames against a state machine — used concretely today in `prog_main.cpp`'s bootloader protocol. + +**Gap to know about**: the actual CAN *transmit* path — whether from `MksServo57D::send_frame()` or from `CAN_Msg_Decoder`'s error-reply TODOs — isn't implemented anywhere yet. The CAN layer is currently receive/decode-only in terms of what's actually wired up, even though the send-side framing logic already exists. + +**In short**: serial = human interactive control/telemetry · RS485 = local sensor feedback (encoders) · CAN = inter-board command/actuation and cross-controller telemetry/bootloading. Three physically and functionally distinct layers. + +--- + +## 17. Where is `HardwareSerial` / `Uart` defined? + +You asked specifically about this — here's the story. `HardwareSerial`/`Uart` are **not part of this repo**; they come from the STM32duino Arduino core, bundled inside the PlatformIO `ststm32` platform package (`framework-arduinoststm32`). That package hasn't been downloaded in this checkout (no `.pio` build dir yet, meaning the project hasn't been built here) — so there's no local file path to point you to until you run a build (`pio run -e engine_controller`), which will fetch it. + +Once fetched, it'll land under something like: +``` +~/.platformio/packages/framework-arduinoststm32/cores/arduino/HardwareSerial.h +~/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32/uart.c (C HAL wrapper) +~/.platformio/packages/framework-arduinoststm32/libraries/SrcWrapper/src/stm32/uart.cpp +``` +(not independently verified against this checkout — verify once you've built). + +**Why `Uart` and not `HardwareSerial`?** This repo's own git history explains it — three relevant commits: +``` +84d8c1b switch to ststm32 platform version 20.0.0 change all uses of HardwareSerial to Uart due to breaking change on stm32duino version 3.0.0 +bf39458 pin platform due to breaking HardwareSerial changes +d8a0580 make the linker error go away that is caused by missing _write definition +``` +STM32duino's core made a breaking change in v3.0.0: `HardwareSerial` became the abstract/base API class (inherited from ArduinoCore-API), and `Uart` became STM32's concrete subclass that application code should instantiate directly. `platformio.ini` pins `platform = ststm32 @ 20.0.0` specifically to lock in this version of the core. Every UART object in this codebase — `ec_main.cpp`'s `HW_CommsSerial`, `HW_FallbackSerial`, `RS485_6`, `RS485_2`; `ec_pins.h`'s `extern Uart RS485_6;`; `AMT242AV`'s `Uart &uart` constructor arg — consistently uses `Uart`, confirming the migration was applied throughout. + +--- + +## 18. Known stubs / TODOs / bugs worth knowing before you dive in + +A consolidated list, so you don't have to rediscover these: + +| Location | Issue | +|---|---| +| `lib/RS485/RS485.h` | Empty stub class, missing terminating `;`, not referenced anywhere. | +| `lib/tvc_actuators/TVC_Actuators.cpp` | `set_angles_pitch_yaw()` computes lengths but does nothing with them — pure stub. | +| `lib/valve_controller/ValveController.cpp` | `get_controller_output()` always returns `{0, 0}` — the core throttle control loop is unimplemented. | +| `lib/throttle_valves/MksServo57D.cpp` | `send_frame()` builds the CAN frame but never transmits it — `set_speed()` has no hardware effect. | +| `lib/throttle_valves/ThrottleValves.cpp` | `begin()` always returns `true` — no real health check, unlike PressureSensors/TemperatureSensors. | +| `lib/throttle_valves/ThrottleValves.cpp` | `set_position()` is P-only control with an untuned `K = 1`; no PID yet. | +| `lib/can_bus/toad_can_bus.h` | `CAN_Msg_Decoder`'s error-reply paths are all `// TODO - send` — no CAN transmit path implemented anywhere. | +| `lib/temperature_sensors/Adafruit_MAX31856.cpp` | Chip-select toggled via `pinMode(cs_pin, HIGH/LOW)` instead of `digitalWrite()` — likely a bug carried from upstream Adafruit code. | +| `lib/temperature_sensors/TemperatureSensors.cpp` | `print_tc_readings()` prints Fahrenheit values but labels them `C` in the format string. | +| `lib/pressure_sensors/pt_calibration.h` | All PT calibration constants are placeholders (`slope=0, offset=10`). | +| `lib/pressure_sensors/PressureSensors.cpp` | Raw ADC→engineering-units conversion flagged as incomplete (missing voltage-level/2²⁴ scaling). | +| `lib/tvc_actuators/GimbalKinematics.cpp` | All actuator geometry constants marked `// TODO - update these` — computed lengths not yet trustworthy. | +| `src/prog_main.cpp` | CAN RX payload parsing (`raw_bytes`/`raw_msg_len`) never actually populated; likely `chunk_rcv[i]` vs `chunk_rcv` array-truthiness bug in the page-write state machine. | +| `lib/throttle_valves/AMT242AV.cpp` | Header flags `// TODO - audit code below and update for EC` — ported from a prior "Portenta" board, not fully validated on TOAD hardware. | +| `src/fc_main.cpp` | Entire FC firmware is a two-line stub. | + +--- + +*Generated as an orientation aid — verify against the current source before relying on specifics, since this is a fast-moving embedded codebase and several of the "stub"/"TODO" items above are exactly the kind of thing that gets fixed without this doc being updated.* diff --git a/firmware/lib/RS485/RS485.cpp b/firmware/lib/RS485/RS485.cpp new file mode 100644 index 0000000..b9b7e68 --- /dev/null +++ b/firmware/lib/RS485/RS485.cpp @@ -0,0 +1,170 @@ +#include "RS485.h" +#include "CommsSerial.h" +#include "ec_pins.h" +#include + +namespace { +constexpr uint32_t kMarginUs = 200; // tune against response time +constexpr uint32_t kTxSlackUs = 2000; +} // namespace + +RS485Bus::RS485Bus(Uart &uart, const uint32_t *sels, size_t sel_count) + : uart_(uart), sels_(sels), sel_count_(sel_count) {} + +bool RS485Bus::begin(uint32_t baud) { + for (size_t i = 0; i < sel_count_; i++) { + pinMode(sels_[i], OUTPUT); + digitalWrite(sels_[i], LOW); + } + + uart_.begin(baud); // core muxes RX/TX/DE (as RTS) and sets RTSE + if (!uart_) + return false; + return setBaud(baud); // also converts RTS flow control -> DE mode +} + +bool RS485Bus::setBaud(uint32_t baud) { + if (baud == 0) + return false; + if (!waitTxComplete(frameTimeUs(SERIAL_TX_BUFFER_SIZE) + kTxSlackUs)) + return false; + + UART_HandleTypeDef *h = uart_.getHandle(); + USART_TypeDef *u = h->Instance; + + h->Init.BaudRate = baud; + h->Init.HwFlowCtl = UART_HWCONTROL_NONE; // stop UART_SetConfig from re-enabling RTSE + + noInterrupts(); + // UART_SetConfig clears the HAL ISR pointers the core's IT-mode RX depends on. + auto rx_isr = h->RxISR; + auto tx_isr = h->TxISR; + + u->CR1 &= ~USART_CR1_UE; // BRR/DE fields writable only with UE = 0 + bool ok = (UART_SetConfig(h) == HAL_OK); // BRR + PRESC from the real clock source + applyDE(u); + u->CR1 |= USART_CR1_UE; + + h->RxISR = rx_isr; + h->TxISR = tx_isr; + interrupts(); + + while (uart_.available()) + uart_.read(); // anything framed at the old rate is garbage + if (ok) + baud_ = baud; + return ok; +} + +void RS485Bus::applyDE(USART_TypeDef *u) { + u->CR3 &= ~(USART_CR3_RTSE | USART_CR3_DEP); // no RTS flow control, DE active-high + u->CR3 |= USART_CR3_DEM; + u->CR1 &= ~(USART_CR1_DEAT | USART_CR1_DEDT); + + // DEAT = 31/16 bit: gives the transceiver time to enable + u->CR1 |= (31U << USART_CR1_DEAT_Pos) | (1U << USART_CR1_DEDT_Pos); +} + +bool RS485Bus::deModeActive() const { + return (uart_.getHandle()->Instance->CR3 & USART_CR3_DEM) != 0; +} + +void RS485Bus::deselectAll() { + for (size_t i = 0; i < sel_count_; i++) { + digitalWrite(sels_[i], LOW); + } +} + +void RS485Bus::select(size_t idx) { + deselectAll(); + if (idx < sel_count_) { + digitalWrite(sels_[idx], HIGH); + } +} + +// Same condition Uart::flush() waits on (tx_tail advances in the TC callback), but bounded. +bool RS485Bus::waitTxComplete(uint32_t timeout_us) { + const uint32_t start = micros(); + while (uart_.availableForWrite() < SERIAL_TX_BUFFER_SIZE - 1) { + if (micros() - start >= timeout_us) + return false; + } + return true; +} + +uint32_t RS485Bus::frameTimeUs(size_t len) const { + if (baud_ == 0) + return 0; + return (uint32_t)((len * 10ULL * 1000000ULL) / baud_); +} + +// RS485Device methods + +bool RS485Device::beginTransaction() { + bus_.deselectAll(); + bool ok = true; + if (bus_.baud_ != baud_) + ok = bus_.setBaud(baud_); + + bus_.select(idx_); + + while (uart.available()) + uart.read(); + + return ok; +} + +size_t RS485Device::read(uint8_t *dst, size_t len, uint32_t latency_us) { + // Don't start response clock while our own request is still sending + bus_.waitTxComplete(bus_.frameTimeUs(SERIAL_TX_BUFFER_SIZE) + kTxSlackUs); + + const uint32_t budget = latency_us + bus_.frameTimeUs(len) + kMarginUs; + const uint32_t start = micros(); + size_t n = 0; + while (n < len) { + if (uart.available()) { + dst[n++] = (uint8_t)uart.read(); + continue; + } + if (micros() - start >= budget) + break; + } + return n; +} + +void RS485Device::endTransaction() { + bus_.waitTxComplete(bus_.frameTimeUs(SERIAL_TX_BUFFER_SIZE) + kTxSlackUs); + bus_.deselectAll(); +} + +// Namespace Globals + +namespace RS485s { +namespace { +constexpr uint32_t kEncBaud = 2000000; // AMT24 2 Mbps data rate +constexpr uint32_t kTvcBaud = 2000000; // TODO - Check Baud rate for TVC + +// Index in each array == device index below +const uint32_t bus6_sels[] = {PIN_TVC_PITCH_SEL, PIN_ENC_OX_SEL}; +const uint32_t bus2_sels[] = {PIN_TVC_YAW_SEL, PIN_ENC_FU_SEL}; +} // namespace + +RS485Bus bus6(RS485_6, bus6_sels, std::size(bus6_sels)); +RS485Bus bus2(RS485_2, bus2_sels, std::size(bus2_sels)); + +RS485Device tvc_pitch(bus6, 0, kTvcBaud); +RS485Device enc_ox(bus6, 1, kEncBaud); +RS485Device tvc_yaw(bus2, 0, kTvcBaud); +RS485Device enc_fu(bus2, 1, kEncBaud); + +bool begin() { + bool ok = true; + ok &= bus6.begin(kEncBaud); // start at the in-flight rate + ok &= bus2.begin(kEncBaud); + ok &= bus6.deModeActive(); + ok &= bus2.deModeActive(); + if (!ok) + CommsSerial.println("ERROR: RS485 init failed"); + return ok; +} +} // namespace RS485s \ No newline at end of file diff --git a/firmware/lib/RS485/RS485.h b/firmware/lib/RS485/RS485.h new file mode 100644 index 0000000..192ff24 --- /dev/null +++ b/firmware/lib/RS485/RS485.h @@ -0,0 +1,69 @@ +#pragma once +#include +#include +#include + + +// DE pin is passed to the Uart constructor as RTS so that the core muxes it, +// and RS485Bus converts RTS flow control into DE mode. +// NOTE: DO NOT call uart.begin() on a bus after RS485s::begin() since it will re-enable RTSE and kill DE. +class RS485Bus { +public: + RS485Bus(Uart &uart, const uint32_t *sels, size_t sel_count); + + bool begin(uint32_t baud); // once, at startup + bool setBaud(uint32_t baud); // waits for TX to finish + uint32_t baud() const { return baud_; } + + void deselectAll(); + bool waitTxComplete(uint32_t timeout_us); + uint32_t frameTimeUs(size_t len) const; // 8N1: 10 bits per byte + bool deModeActive() const; + +private: + Uart &uart_; + const uint32_t *sels_; + size_t sel_count_; + uint32_t baud_ = 0; + + void select(size_t idx); + static void applyDE(USART_TypeDef *u); // requires UE = 0 + + friend class RS485Device; +}; + +class RS485Device { +public: + RS485Device(RS485Bus &bus, size_t idx, uint32_t baud) + : uart(bus.uart_), bus_(bus), idx_(idx), baud_(baud) {} + + // Switches bus to this device's baud if needed, selects it, flushes stale RX. + // Returns false if baud switch failed. + bool beginTransaction(); + void endTransaction(); + + // Returns number of bytes received (== len on success). + size_t read(uint8_t *dst, size_t len, uint32_t latency_us = 500); + + void setBaud(uint32_t baud) { baud_ = baud; } // applied at next beginTransaction() + uint32_t baud() const { return baud_; } + + Uart &uart; + +private: + RS485Bus &bus_; + size_t idx_; + uint32_t baud_; +}; + +namespace RS485s { +bool begin(); + +extern RS485Bus bus6; +extern RS485Bus bus2; + +extern RS485Device tvc_pitch; +extern RS485Device enc_ox; +extern RS485Device tvc_yaw; +extern RS485Device enc_fu; +} // namespace RS485s \ No newline at end of file diff --git a/firmware/src/ec_main.cpp b/firmware/src/ec_main.cpp index c45a242..2f24cb0 100644 --- a/firmware/src/ec_main.cpp +++ b/firmware/src/ec_main.cpp @@ -11,12 +11,14 @@ #include "ThrottleValves.h" #include "ValveController.h" #include "fdcan_toad.h" +#include "RS485.h" // shared interfaces CommsSerial_t USB_CommsSerial; CommsSerial_t HW_CommsSerial(PIN_HW_COMM_SERIAL_RX, PIN_HW_COMM_SERIAL_TX); CommsSerial_t HW_FallbackSerial(PIN_HW_FALLBACK_SERIAL_RX, PIN_HW_FALLBACK_SERIAL_TX); -// TODO - configure DE pin + +// DE is passed as RTS and is converted to hardware DE mode by RS485Bus; DO NOT call begin() on these. Uart RS485_6(PIN_RS485_6_RX, PIN_RS485_6_TX, PIN_RS485_6_DE); Uart RS485_2(PIN_RS485_2_RX, PIN_RS485_2_TX, PIN_RS485_2_DE); @@ -35,10 +37,6 @@ void setup() { HW_CommsSerial.begin(RADIO_BAUD); HW_FallbackSerial.begin(RADIO_BAUD); - - RS485_6.begin(9600); // TODO - what baud? - RS485_2.begin(9600); - PT_TC_SPI_1.begin(); PT_TC_SPI_3.begin(); @@ -55,6 +53,7 @@ void setup() { all_modules_ok &= CAN::init(); all_modules_ok &= PressureSensors::begin(); all_modules_ok &= TemperatureSensors::begin(); + all_modules_ok &= RS485s::begin(); all_modules_ok &= ThrottleValves::begin(); all_modules_ok &= SolenoidValves::begin(); all_modules_ok &= TVC_Actuators::begin(); From d9e1821e05f5755ffd0fc905de5d182819b854e2 Mon Sep 17 00:00:00 2001 From: "Programmer.Lotus" <77867932+Rishabg24@users.noreply.github.com> Date: Thu, 17 Sep 2026 06:08:52 -0700 Subject: [PATCH 02/10] Branch Cleanup --- firmware/CODEBASE_GUIDE.md | 754 ------------------------------------- 1 file changed, 754 deletions(-) delete mode 100644 firmware/CODEBASE_GUIDE.md diff --git a/firmware/CODEBASE_GUIDE.md b/firmware/CODEBASE_GUIDE.md deleted file mode 100644 index 4fde535..0000000 --- a/firmware/CODEBASE_GUIDE.md +++ /dev/null @@ -1,754 +0,0 @@ -# TOAD Firmware — Codebase Guide - -A from-scratch orientation to `firmware/` for engineers who are new to this repo. Scope: embedded firmware only (`firmware/`) — the UI directory is intentionally excluded. This is a snapshot as of commit `4a25a3b` on branch `features/RS485`; treat any "currently a stub" / "TODO" note as something that may have changed since — check the file before relying on it. - ---- - -## 1. The big picture - -This is **one PlatformIO project that builds three separate firmware images** from a shared `src/` and `lib/` tree: - -| PlatformIO env | Board | Compiles | Role | -|---|---|---|---| -| `engine_controller` | `TOAD_H7` | `src/ec_main.cpp` (+ any `ec_*.cpp`) | **Engine Controller (EC)** — the main, most-developed firmware. Reads pressure/temperature sensors, drives throttle valves, TVC actuators, RCS thrusters, and solenoid/ball valves. Runs the flight control loop. | -| `flight_controller` | `TOAD_H7` (same board type as EC "for now") | `src/fc_main.cpp` (+ any `fc_*.cpp`) | **Flight Controller (FC)** — currently a stub, not yet implemented. | -| `programmer` | `TOAD_G4` (different, smaller MCU) | `src/prog_main.cpp` (+ any `prog_*.cpp`) | **Programmer board** — a CAN-to-SPI bridge used to remotely flash new firmware onto an EC or FC H7 board over the GSE CAN bus, without a physical debugger. | - -This selection happens via `build_src_filter` in [`platformio.ini`](platformio.ini) — each env excludes all `.cpp` files then re-includes only the ones matching its prefix. `lib_ldf_mode = chain+` means PlatformIO only pulls in a `lib/*` folder if something in the active build actually `#include`s it, so each env only compiles the libraries it needs. - -**If you're new, start here, in this order:** -1. [`platformio.ini`](platformio.ini) — see what actually gets built. -2. [`lib/hardware_mapping/ec_pins.h`](lib/hardware_mapping/ec_pins.h) — the map of "what's plugged into which pin." -3. [`src/ec_main.cpp`](src/ec_main.cpp) — the entry point that wires everything together. -4. Then dip into whichever `lib/` subsystem you're touching. - ---- - -## 2. `platformio.ini` - -Single manifest. A shared `[env]` base plus the three environments above. - -```ini -[env] -platform = ststm32 @ 20.0.0 -framework = arduino -build_flags = - -D SERIAL_RX_BUFFER_SIZE=1024 ; increase serial buffer size - -D SERIAL_TX_BUFFER_SIZE=1024 - -Wl,-u,_printf_float - -Wl,-u,_scanf_float - -D USBCON - -D USBD_USE_CDC - -D RADIO_BAUD=57600 ; used on all Serial interfaces for consistency, see monitor_speed above - -O3 ; compile for speed -``` - -- `platform = ststm32 @ 20.0.0` is **pinned deliberately** — see [§9, the `Uart`/`HardwareSerial` story](#9-where-is-hardwareserial--uart-defined) for why. -- `RADIO_BAUD=57600` is the one baud rate used consistently across every UART in the firmware. -- Each env adds its own define (`TOAD_ENGINE_CONTROLLER_ONLY`, `TOAD_FLIGHT_CONTROLLER_ONLY`, `TOAD_PROGRAMMER_ONLY`) and its own `build_src_filter`. -- The `flight_controller` env has a TODO: `; TODO - add DFU support, and "upload over CAN" if we are feeling clever.` - ---- - -## 3. `src/` — the three entry points - -### `ec_main.cpp` — Engine Controller (the main firmware) - -This is the file that owns and wires together nearly every subsystem in `lib/`. Read this file first for the real picture; everything below is a map of what it touches. - -**Global hardware objects declared here** (and `extern`-referenced from `ec_pins.h`): - -```cpp -CommsSerial_t USB_CommsSerial; -CommsSerial_t HW_CommsSerial(PIN_HW_COMM_SERIAL_RX, PIN_HW_COMM_SERIAL_TX); -CommsSerial_t HW_FallbackSerial(PIN_HW_FALLBACK_SERIAL_RX, PIN_HW_FALLBACK_SERIAL_TX); -// TODO - configure DE pin -Uart RS485_6(PIN_RS485_6_RX, PIN_RS485_6_TX, PIN_RS485_6_DE); -Uart RS485_2(PIN_RS485_2_RX, PIN_RS485_2_TX, PIN_RS485_2_DE); - -SPIClass PT_TC_SPI_1(PIN_PT_TC_SPI_1_MOSI, PIN_PT_TC_SPI_1_MISO, PIN_PT_TC_SPI_1_SCK); -SPIClass PT_TC_SPI_3(PIN_PT_TC_SPI_3_MOSI, PIN_PT_TC_SPI_3_MISO, PIN_PT_TC_SPI_3_SCK); -``` - -Note the live `// TODO - configure DE pin` sitting right above the RS485 UART construction — RS485 direction control via the 3rd constructor arg isn't verified yet. - -**Control flow — this is not a plain Arduino sketch.** `loop()` is deliberately minimal: - -```cpp -void loop() { - while (CommsSerial.available()) { - CommandRouter::receive_byte(CommsSerial.read()); - } -} -``` - -It just pumps bytes from the primary serial into the command router. The real flight logic lives in a separate **`flight_loop()`** function that is registered as a CLI command (`CommandRouter::add(flight_loop, "start_flight_loop")`) and only executes once an operator types that command over serial. `flight_loop()` is a blocking `while(true)`: - -1. Resets `kill_flag`/`arm_flag`, drives throttle valves to a 30° starting angle, zeroes TVC, closes RCS. -2. Loop body: drains any pending serial commands (so `kill_flag` can be set mid-flight via a `"k"` command), reads PT/TC sensors, calls `ValveController::get_controller_output()`, and — **only if `arm_flag` is set** — actually commands the throttle valves, TVC actuators, and RCS. -3. On `kill_flag`, breaks out and safes the throttle valves + RCS. - -Two `CommandRouter::add_flag` calls wire the CLI commands `"k"` and `"arm"` to `kill_flag`/`arm_flag`, so an operator can arm or abort the flight loop live. - -`setup()`: starts all comm serials at `RADIO_BAUD`, RS485 buses at 9600 (`// TODO - what baud?`), SPI buses, waits 3s, prints a startup banner to both `CommsSerial` (macro, see §9 CommsSerial section) and the fallback serial, then calls every subsystem's `begin()` and ANDs the results into `all_modules_ok`. **If any module fails init, the firmware halts in an infinite loop printing an error every 5s rather than proceeding** — a deliberate fail-safe. - -Depends on: `CommandRouter.h`, `CommsSerial.h`, `PressureSensors.h`, `RCS.h`, `SolenoidValves.h`, `TVC_Actuators.h`, `TemperatureSensors.h`, `ThrottleValves.h`, `ValveController.h` (which transitively pull in `ec_pins.h`, `ec_sensors.h`, `ec_valves.h`, `toad_can_bus.h`). - -### `fc_main.cpp` — Flight Controller (stub, not yet implemented) - -```cpp -void setup() -{ - Serial.begin(9600); - Serial.println("init"); // had to add this; for some reason without it we don't pull in Print.cpp which causes a linker error because _write is not defined -} -void loop() {} -``` - -That comment documents a real STM32duino linker quirk: without at least one `Serial.println()` call, `Print.cpp` never gets linked in, and `_write` (used by libc stdio) ends up undefined, breaking the link. The `#include "CommsSerial.h"` at the top is commented out — FC doesn't use the shared comms layer yet. - -### `prog_main.cpp` — Programmer board (CAN-to-SPI bootloader bridge) - -Sits between the GSE CAN bus and an STM32H7 target (EC or FC board) and can force that H7 into its ST SPI bootloader to flash it without a debugger or USB cable. Explicitly implements ST's app note **AN4286** ("How to use SPI protocol in bootloader on STM32 MCUs") — comments are literally annotated with page citations, e.g. `// Send SYNC byte and get ACK [6]`, `// Mass erase [26]`. - -Key protocol constants: -```cpp -constexpr uint8_t ACK = 0x79; -constexpr uint8_t NACK = 0x15; -constexpr uint8_t SYNC = 0x5A; -constexpr uint8_t CMD_WriteMem = 0x31; -constexpr uint8_t CMD_EraseMem = 0x44; -``` - -Flash paging: -```cpp -// 2 MB of Flash divided into 512 x 4096 byte virtual pages. -// Chunks for the active page are received 32 bytes at a time, for 128 chunks per page. -// The active page is written out 256 bytes at a time. -constexpr size_t CAN_CHUNK_SIZE = 32; -constexpr size_t WRITE_CHUNK_SIZE = 256; -constexpr size_t PAGE_CACHE_SIZE = 4096; -constexpr size_t NUM_PAGE_CACHES = 512; -static_assert(PAGE_CACHE_SIZE * NUM_PAGE_CACHES == 2097152); -``` - -State machine: `enum prog_state_t { STATE_IDLE, STATE_PRE_ERASE, STATE_READY, STATE_PAGE_SELECTED }`. - -One physical programmer image serves **both** EC and FC boards — at boot it reads a GPIO strap pin to decide which target it's talking to: `prog_type = (digitalRead(PIN_PROG_ID) == PROG_ID_FLIGHT_CONTROLLER) ? PROG_FLIGHT_CONTROLLER : PROG_ENGINE_CONTROLLER;` - -Key functions: `reset_h7()` (pulses NRST), `spi_ack_frame()` (flagged `// TODO - pretty sure sometimes this has to cycle around until ACK`), `enter_bootloader()`, `erase_memory()`, `write_memory(addr, bytes, len)`. - -`loop()` is a CAN-message-driven state machine built on the `CAN_Msg_Decoder` template from `toad_can_bus.h` (see §5). **Known incomplete/buggy areas**: `raw_bytes`/`raw_msg_len` are declared but never actually populated from the CAN peripheral (`// TODO - figure out how to parse the incoming cmd and payload` is left unresolved), and there is a likely real bug where `if (!chunk_rcv)` (an array, always truthy as a pointer) should probably be `if (!chunk_rcv[i])`. - ---- - -## 4. `boards/` — board variant packages (mostly vendor boilerplate) - -`boards/TOAD_G4/` and `boards/TOAD_H7/` are STM32duino Arduino-core "variant" packages — largely auto-generated by ST's CubeMX tooling (BSD-3-Clause, `Copyright (c) 2020, STMicroelectronics`, banner comment `Automatically generated from STM32G473R(B-C-E)Tx.xml ... CubeMX DB release 6.0.160`). You generally won't need to edit these, but it helps to know what's in them: - -- **`PinNamesVar.h`** — alternate-function pin aliases, wakeup pin mappings, USB D+/D- assignments. -- **`variant_TOAD_*.h`** — numeric pin `#define`s, pin counts, default `LED_BUILTIN`, default SPI/I2C/UART pins, `SERIAL_PORT_*` aliasing macros. `variant_TOAD_H7.h` has one project-specific addition at the end: `// Robert additions` → `#define USE_PWR_LDO_SUPPLY`. -- **`variant_TOAD_*.cpp`** — pin lookup tables plus `SystemClock_Config()`. G4 uses HSI+HSI48 oscillators for USB; H7 uses a separate PLL3 tuned specifically for 48 MHz USB (`PLL3M=32, PLL3N=192, PLL3Q=8`) plus `HAL_PWREx_ConfigSupply(PWR_LDO_SUPPLY)`. -- **`PeripheralPins.c`** — ST/CubeMX-generated pin↔peripheral capability tables (ADC, I2C, TIM, UART, SPI, FDCAN, USB, etc.). -- **`ldscript.ld`** — standard linker script; heap/stack sizes reserved via `_Min_Heap_Size`/`_Min_Stack_Size`. - -Practical difference between the boards: G4 defaults to `SERIAL_UART_INSTANCE 2`, H7 to `5`; H7 needs the extra `USE_PWR_LDO_SUPPLY`/PLL3-for-USB tweak that G4 doesn't. - ---- - -## 5. `lib/can_bus/toad_can_bus.h` — CAN topology and message formats - -Defines the CAN network topology, 11-bit CAN IDs, and every custom CAN wire-format message struct. - -Topology, verbatim: -``` -// The primary vehicle CAN bus runs from the flight -// controller to the engine controller and includes -// the power management board. This is a CAN-FD bus. - -// The engine control CAN bus runs from the engine -// controller to the TVC actuators and the stepper -// drivers. This is a CAN 2.0 bus. - -// The GSE CAN bus runs from the GSE, over the QD -// arm and splits to run to each programmer board. -// This is a CAN-FD bus. -``` - -So there are **three physically distinct CAN networks** — this is why `ec_pins.h` defines both `PIN_CAN_TVC_RX/TX` (engine-control CAN 2.0 bus, to TVC/steppers) and `PIN_CAN_FC_RX/TX` (vehicle CAN-FD bus, to the flight controller). - -CAN IDs: -```cpp -// COTS devices (ID range 0x00X) -constexpr uint16_t CAN_ID_TVC_PITCH = 0x001; -constexpr uint16_t CAN_ID_TVC_YAW = 0x002; -constexpr uint16_t CAN_ID_STEPPER_OX = 0x003; -constexpr uint16_t CAN_ID_STEPPER_FU = 0x004; - -// Custom boards (ID range 0x01X) -constexpr uint16_t CAN_ID_FLIGHT_CONTROLLER = 0x011; -constexpr uint16_t CAN_ID_ENGINE_CONTROLLER = 0x012; -constexpr uint16_t CAN_ID_FLIGHT_PROG = 0x013; -constexpr uint16_t CAN_ID_ENGINE_PROG = 0x014; -constexpr uint16_t CAN_ID_GSE = 0x015; -constexpr uint16_t CAN_ID_POWER_BOARD = 0x016; -``` - -Every message struct starts with `uint8_t cmd_id` as a wire discriminator: `can_msg_heartbeat_t` (0x00), error replies `can_msg_invalid_cmd_t`/`can_msg_incorrect_len_t`/`can_msg_unexpected_state_t` (0x01–0x03), telemetry `can_msg_fc_telemetry`/`can_msg_ec_telemetry` (0x10/0x11), and the bootloader protocol messages `can_msg_reset_controller_t`, `can_msg_enter_bootloader_t`, `can_msg_erase_flash_t`, `can_msg_select_page_t`, `can_msg_mem_packet_t`, `can_msg_request_mem_packet_t`, `can_msg_write_flash_t` (0x30–0x36). - -**The one real class here:** - -```cpp -template class CAN_Msg_Decoder { -public: - CAN_Msg_Decoder(const uint8_t *raw_bytes, size_t len, state_t state); - template std::optional decode_and_enforce_state(state_t expected); - template std::optional decode(); // convenience: expected = state - void send_error_if_not_decoded(); -private: - bool decoded; - const uint8_t *raw_bytes; - const size_t len; - const state_t state; -}; -``` - -Usage pattern (as seen in `prog_main.cpp`): -```cpp -if (const auto msg = raw_msg.decode()) { ... } -else if (const auto msg = raw_msg.decode()) { ... } -``` - -`decode_and_enforce_state()` matches `raw_bytes[0]` against `msg_t`'s cmd_id, validates payload length equals `sizeof(msg_t)`, validates the state machine is in the expected state, and on success `memcpy`s into the typed struct. - -**Important gap**: none of the error-struct "send" TODOs are implemented anywhere — there is currently **no actual CAN transmit path** wired up in this decoder or its call sites. It prepares error structs locally and drops them. - ---- - -## 6. `lib/hardware_mapping/` — "what's plugged into which pin" - -This is the authoritative, human-curated source of pin assignments, separate from the low-level Arduino pin numbering in `boards/`. - -### `ec_pins.h` - -Organized by peripheral group, each with explanatory comments: -- **UARTs**: `PIN_HW_COMM_SERIAL_RX/TX` = primary serial (UART5), `PIN_HW_FALLBACK_SERIAL_RX/TX` = fallback (UART3). -- **RS485**: `// RS485 Busses (UART6 and UART2)`; `extern Uart RS485_6; extern Uart RS485_2;` (owned by `ec_main.cpp`), plus per-device bus/select macros: `TVC_PITCH_RS485_BUS`/`PIN_TVC_PITCH_SEL`, `ENC_OX_RS485_BUS`/`PIN_ENC_OX_SEL`, yaw/fuel equivalents on `RS485_2`. `DRV_OX_RS485_BUS`/`PIN_DRV_OX_SEL` marked `// unused`. -- **SPI**: two buses (`PT_TC_SPI_1`, `PT_TC_SPI_3`) shared by the PT and TC boards. -- **QSPI**: pins defined, not obviously used yet. -- **FDCAN**: `PIN_CAN_TVC_RX/TX`, `PIN_CAN_FC_RX/TX`. -- **PWM (spark/igniter)**: `PIN_SPARK_PWM`/`PIN_SPARK_TRIG`. -- **"Zucrow Board" pins**: `// TODO - replace with PT board definition` — placeholder for a separate DI/DO interface board (named for Purdue's Zucrow Labs test stand). -- **Valve DOs**: `NUM_SV_BV_VALVES 16`, `PIN_SV_DO_1..9`, `PIN_BV_DO_10..16`, `PIN_VALVE_OE_INPUT`, `PIN_SV_BV_LATCH_ENABLE` (`// TODO - flywire and assign`). -- **PT/TC boards**: `NUM_PT_BOARDS 6` (each = 1 `ADS131M02` driving 2 PT channels), `NUM_TC_CHIPS 6`, split across the two SPI buses. -- **Utility macros**: `CONCAT`/`STRINGIFY` (classic preprocessor token-paste/stringize helpers) used heavily by `ec_sensors.h`/`ec_valves.h` to build canonical names. - -### `ec_sensors.h` - -Maps numeric PT/TC indices to canonical P&ID-style names, e.g.: -```cpp -#define PT_1 PT_N2_01_tank -#define PT_2 PT_N2_02_reg -... -#define PT_12 PT_FU_05_venturi_upstream -static_assert(NUM_PT_BOARDS == 6); - -#define TC_1 TC_N2_01_tank -... -static_assert(NUM_TC_CHIPS == 6); -``` - -Shared reading structs: -```cpp -struct pressure_readings_t { uint8_t crc_errors; float PT_1..PT_12; }; // all readings in PSI -struct temperature_readings_t { float TC_1..TC_6; }; // all readings in K -``` - -`PT_CALIBRATION(n)` macro expands to e.g. `PT_1_calibration`, defined in `pt_calibration.h`. - -### `ec_valves.h` - -Canonical naming + default fail-safe states for all 16 valve DOs: -```cpp -#define VALVE_SHORT_NAME_LEN 8 // "SV_N2_01" -#define SV_1 SV_N2_01_rcs_pos_1 -... -#define BV_16 BV_FU_03_run -static_assert(NUM_SV_BV_VALVES == 16); - -namespace SolenoidValves { - enum valve_ids { ... }; - enum valve_state_t { VALVE_CLOSE, VALVE_OPEN }; - // A valve state isn't the same as a DO logic level, so use this type to differentiate. - // See valve_state_to_logic_level for more info. -} -``` - -Default positions: N2/O2/FU "release" ball valves default **open** (vent-safe); everything else (RCS, purges, igniters, "run" valves) defaults **closed** (marked `// TODO CONOPS - audit`). Comment in this file: `// Note - this must be kept in sync with the list in SolenoidValves.cpp`. - -### `prog_pins.h` - -Programmer-board (`TOAD_G4`) pins and the boot-control protocol used to bridge to the H7 target: -```cpp -#define PIN_PROG_ID PB1 -#define PROG_ID_FLIGHT_CONTROLLER HIGH -#define PROG_ID_ENGINE_CONTROLLER LOW -enum prog_id_t { PROG_FLIGHT_CONTROLLER, PROG_ENGINE_CONTROLLER }; - -#define PIN_H7_BOOT PC0 -#define BOOT_MODE_RUN LOW -#define BOOT_MODE_FLASH HIGH - -#define PIN_H7_NRST PC1 -#define NRST_MODE_RUN HIGH -#define NRST_MODE_RST LOW -``` -Same firmware image serves both EC and FC — differentiated only by the `PIN_PROG_ID` strap — and directly drives the target H7's BOOT0/NRST pins. - ---- - -## 7. `lib/pressure_sensors/` - -### `ADS131M02.h` / `.cpp` — low-level SPI driver - -TI ADS131M02, a 2-channel 24-bit delta-sigma ADC used to read pressure transducers. (`// Datasheet: https://www.ti.com/lit/ds/symlink/ads131m02.pdf`, citations marked `[pg#]`.) - -```cpp -struct adc_reading_t { uint32_t status_reg; int32_t ch0; int32_t ch1; bool crc_ok; }; - -class ADS131M02 { -public: - ADS131M02(SPIClass spi_bus, unsigned int cs_pin); - void begin(); - adc_reading_t read_adc(); -private: - uint32_t transact_word(uint32_t cmd, uint8_t *crc_buf); - SPIClass spi_bus; - unsigned int cs_pin; -}; -``` - -`SPISettings ADS131M02_SPI_SETTINGS(4000000, MSBFIRST, SPI_MODE1);` with `// TODO - verify clock integrity with a scope.` `read_adc()` sends the ADC's "NULL" command 4 times (status/ch0/ch1/CRC words per frame), computes a CRC-16 (poly `0x1021`) to validate, and sign-extends the 24-bit two's-complement channel values. Frame protocol comment: `// The ADS131M02 communicates in 24 bit words, grouped into 4 word frames ... This function should be called 4 times to process a complete frame.` - -### `PressureSensors.h` / `.cpp` — application layer - -```cpp -class PT_Board { -public: - PT_Board(SPIClass &spi_bus, unsigned int cs_pin, float pt0_slope, float pt0_offset, float pt1_slope, float pt1_offset); - void begin(); - bool read_pts(float *pt0_reading, float *pt1_reading); -private: - ADS131M02 adc; - float pt0_slope, pt0_offset, pt1_slope, pt1_offset; -}; - -namespace PressureSensors { - bool begin(); - pressure_readings_t read_pts(); - void print_pt_crc_errors(pressure_readings_t pt_readings); - void print_pt_readings(); -} -``` - -Instantiates all 6 `PT_Board`s using the `PT_CALIBRATION(n)` macro for slope/offset. `read_pts()` has a live TODO: `// TODO PTs - add preconversion logic to account for voltage level changes / probably need to divide by pow(2, 24) as well` — today's raw-code→engineering-units conversion is a naive linear `slope*raw+offset` and is flagged as incomplete. `begin()` does an initial read; if CRC errors are found at boot it prints them and **returns false**, which propagates into `ec_main.cpp`'s `all_modules_ok` fail-safe halt. Registers CLI command `print_pt`. - -### `pt_calibration.h` - -Per-sensor linear calibration constants — **currently all placeholders**: every PT is `slope=0, offset=10`. Don't trust any PT reading until these are filled in with real calibration data. - ---- - -## 8. `lib/RS485/` — new, untracked, effectively empty (relevant to your current branch) - -This is the directory shown as untracked in `git status` on `features/RS485`. - -```cpp -// RS485.h -#pragma once -#include -#include -#include - -class RS485{ - -} -``` - -This is a stub: the class body is empty, and **the class definition is missing its terminating semicolon** — `class RS485{ ... }` with no `;` — which will fail to compile if this header is ever `#include`d and the type used. `RS485.cpp` is completely empty (0 bytes). - -Nothing else in the codebase references this file yet. Today, RS485 buses are implemented directly as plain `Uart` objects with a manually-toggled `SEL`/DE pin (see `ec_main.cpp`'s `RS485_6`/`RS485_2`, and `AMT242AV`'s hand-rolled RS485 bit-banging below) — this library looks like the start of an effort to factor that pattern out into a reusable class, but it isn't wired into anything yet. - ---- - -## 9. `lib/serial_comms/` - -### `CommsSerial.h` - -Template wrapper adding buffered `readline()`, `scanf`-style parsing, and `printf`-style convenience methods on top of any Arduino serial-like class (`Uart`, `USBSerial`). Also declares the two shared comm-serial globals used everywhere. - -```cpp -#define PRINT_BUFFER_SIZE 1024 -#define READ_BUFFER_SIZE 1024 - -template class CommsSerial_t : public BaseSerial { -public: - using BaseSerial::BaseSerial; - char readbuf[READ_BUFFER_SIZE]; - char *readline(); - template int scanf(const char *format, Args... args); - template void printf(const char *format, Args... args); - template void mprint(const T &t); - template void mprint(const T &t, const Args &...args); - template void mprintln(const Args &...args); -}; - -extern CommsSerial_t HW_CommsSerial; -extern CommsSerial_t USB_CommsSerial; - -#define CommsSerial HW_CommsSerial -``` - -**Important gotcha**: `CommsSerial` is a **macro**, not a variable — `#define CommsSerial HW_CommsSerial`. This is why code all over the tree (`PressureSensors.cpp`, `SolenoidValves.cpp`, etc.) can just write `CommsSerial.println(...)` and have it transparently mean "the primary hardware comm UART." Because it's a preprocessor macro, it can't be locally shadowed or reassigned — keep that in mind if you ever want a function-local variable named `CommsSerial`. - -`readline()` busy-waits reading characters until `\n`, with `\b` as a destructive backspace. `printf` snprintfs into a 1 KB stack buffer, then calls `BaseSerial::print()`. - -### `CommandRouter.h` / `.cpp` - -A newline-delimited ASCII command router over `CommsSerial`, with escaped control characters, used both as an interactive CLI for GSE/test operators and as the registration point for firmware modules' own commands (`print_pt`, `open_valve`, `start_flight_loop`, etc.) and flags (`k`, `arm`). - -```cpp -struct command { - std::function f; - const char *name; - const char *help; -}; - -#define END_CHAR '\n' -#define CR_CHAR '\r' -#define ESCAPE_CHAR '\\' -#define BACKSPACE_CHAR '\b' - -namespace CommandRouter { - void begin(); - void receive_byte(uint8_t c); - template void send_command(const char *command, S data); - void help(const char *cmd_name); - void add(std::function f, const char *name, const char *help = "no help provided"); - void add(std::function fstr, const char *name, const char *help = "no help provided"); - void add(std::function fvoid, const char *name, const char *help = "no help provided"); - void add_flag(bool *flag, const char *name, const char *help); -} -``` - -`receive_byte()` is a byte-at-a-time state machine (fed from `ec_main.cpp`'s `loop()`) that builds a command buffer (max `MAX_CMD_LEN 1024`), handles `\n` (dispatch), `\r` (ignored — `// do nothing - we aren't a typewriter, no need to carriage return`), `\b` (backspace), and `\` (escape, so binary payloads like `sv_ui`'s 4-byte bitmask can embed the 4 special bytes literally). `help()` supports `help` (list all), `help `, and `help ` (prefix match — `// no command found, so print all commands that start with what user entered`). `send_command()` is the outbound side: writes a command name plus a raw binary payload, escaping special bytes, for sending structured telemetry to a host. Built-in commands: `help`, and `ping` (`CommsSerial.println("pong")`, connectivity check). - ---- - -## 10. `lib/solenoid_valves/` - -### `SolenoidValves.h` / `.cpp` - -Drives the 16 solenoid/ball-valve digital outputs through what's implied to be a latching relay driver (based on the `pulse_latch_enable()` pattern), converting between abstract `VALVE_OPEN`/`VALVE_CLOSE` and the correct GPIO level per valve. - -```cpp -namespace SolenoidValves { - bool begin(); - void pulse_latch_enable(); - void set_valves_from_valve_state(uint32_t valve_states); - void set_valves_from_valve_state_cmd(const uint8_t *cmd_packet, size_t len); - void set_valve_by_num(int i, valve_state_t state, bool pulse_latch = true); - void open_valve_by_name(const char *name); - void close_valve_by_name(const char *name); -} -``` - -**Key safety design note — read this if you touch valve code.** Verbatim: -```cpp -// Converts from VALVE_OPEN / VALVE_CLOSE to LOW / HIGH depending on the valve wiring. -// All valves must enter a 'default / safe' state when they receive a LOW signal. -// This requirement is driven by the flight termination system functionality. -// So to check whether the DO should be LOW or HIGH, just compare against this default state. -bool valve_state_to_logic_level(int valve_num, valve_state_t target_state) { - return target_state == sv_and_bvs[valve_num].default_state ? LOW : HIGH; -} -``` -**LOW always means safe**, regardless of whether "safe" happens to be open or closed for a given valve. A wire break, power loss, or FTS (Flight Termination System) trigger naturally drives every valve pin LOW and lands it in its safe configuration. - -`begin()`: leaves `PIN_SV_BV_LATCH_ENABLE` LOW at boot (`// On boot, leave all valves in the state they were left in by setting latch enable low.`), configures `PIN_VALVE_OE_INPUT` as input (`// TODO - check state of this pin to see if flight has been terminated`), sets every valve pin LOW+OUTPUT, registers CLI commands `open_valve`, `close_valve`, `sv_ui` (binary bitmask command). - -`pulse_latch_enable()` (`// TODO - test this delay / check datasheet`) pulses latch-enable HIGH for 50µs then LOW — this is what actually propagates buffered DO states out to the physical valve driver hardware, implying valve outputs go through a latching driver IC rather than direct GPIO. - -### `RCS.h` / `.cpp` — Reaction Control System - -Simple bang-bang/deadband control of 4 N2 attitude-thruster solenoid valves. - -```cpp -#define RCS_DEADBAND 1 // N? - -namespace RCS { - void close(); - void update_rcs_valves(float rcs_force); -} -``` - -`update_rcs_valves(rcs_force)`: `>= RCS_DEADBAND` opens "pos" valves + closes "neg"; `<= -RCS_DEADBAND` the reverse; otherwise closes all. Each branch sets 4 valve DOs individually with `pulse_latch=false` then calls `pulse_latch_enable()` once, batching the latch pulse. - ---- - -## 11. `lib/temperature_sensors/` - -### `Adafruit_MAX31856.h` / `.cpp` - -SPI driver for the MAX31856 thermocouple amplifier, a trimmed fork of Adafruit's library: `// Modifed by Robert Nies to remove dependency on Adafruit_SPIDevice` / `// We only use Hardware SPI on Toad`. - -```cpp -class Adafruit_MAX31856 { -public: - Adafruit_MAX31856(SPIClass &spi_bus, unsigned int cs_pin, max31856_thermocoupletype_t tc_type); - bool begin(void); - void setConversionMode(max31856_conversion_mode_t mode); - max31856_conversion_mode_t getConversionMode(void); - void setThermocoupleType(max31856_thermocoupletype_t type); - max31856_thermocoupletype_t getThermocoupleType(void); - uint8_t readFault(void); - void triggerOneShot(void); - bool conversionComplete(void); - float readCJTemperature(void); - float readThermocoupleTemperature(void); - void setTempFaultThreshholds(float flow, float fhigh); - void setColdJunctionFaultThreshholds(int8_t low, int8_t high); - void setNoiseFilter(max31856_noise_filter_t noiseFilter); -private: - SPIClass &spi_bus; unsigned int cs_pin; - max31856_conversion_mode_t conversionMode; - max31856_thermocoupletype_t tc_type; - void readRegisterN(uint8_t addr, uint8_t buffer[], uint8_t n); - uint8_t readRegister8(uint8_t addr); - uint16_t readRegister16(uint8_t addr); - uint32_t readRegister24(uint8_t addr); - void writeRegister8(uint8_t addr, uint8_t reg); -}; -``` - -`SPISettings Adafruit_MAX31856_SPI_SETTINGS(4000000, MSBFIRST, SPI_MODE1);` (same unverified-clock TODO as `ADS131M02`). `begin()` enables open-circuit fault detection, zeroes cold-junction offset, sets thermocouple type, enables continuous conversion, and validates connectivity via a register readback of the factory-default `0xC0`. `readThermocoupleTemperature()` reads a 24-bit signed register, sign-extends, shifts off the unused bottom 5 bits, and scales by `0.0078125` (2⁻⁷) to get °C. - -**Bug worth knowing about**: `readRegisterN()` and `writeRegister8()` call `pinMode(cs_pin, LOW)` / `pinMode(cs_pin, HIGH)` to toggle chip-select. This should almost certainly be `digitalWrite()`, not `pinMode()` — likely a copy-paste artifact from the upstream Adafruit source. Whether it actually works depends on this STM32duino `pinMode()` implementation tolerating a non-mode second argument, which is fragile. Worth fixing if you're in this file. - -### `TemperatureSensors.h` / `.cpp` - -```cpp -namespace TemperatureSensors { - bool begin(); - temperature_readings_t read_tcs(); - void print_tc_readings(); -} -``` - -Instantiates all 6 K-type thermocouple channels across the two shared SPI buses. `#define C_TO_KELVIN 273.15`; `read_tcs()` converts to Kelvin (matches `ec_sensors.h`'s `// all readings in K`). - -**Inconsistency worth knowing about**: `print_tc_readings()` (whose own comment says `// print TC readings in Fahrenheit.` and which does call `c_to_f()`) prints using a format string labeled `%6.2f C` — the unit *label* in the printf format is wrong (says C, prints F); the conversion itself is correct. - ---- - -## 12. `lib/throttle_valves/` - -### `AMT242AV.h` / `.cpp` - -Driver for a CUI AMT242A-V absolute magnetic encoder over half-duplex RS485 (`Uart` + a `SEL` GPIO toggling a MAX485 transceiver between TX/RX), used to read throttle-valve shaft position. - -```cpp -class AMT242AV { -public: - AMT242AV(Uart &uart, unsigned int SEL, uint8_t ID); - void begin(); - bool read_pos(float *out, int max_retries = 10); - void zero(); - void reset(); -private: - Uart &uart; unsigned int SEL; uint8_t ID; - bool wait_for_avail(unsigned long long); - bool _read_pos(uint16_t *); -}; -``` - -`#define MAX_READING ((1 << 12) - 1)` — 12-bit encoder resolution. `_read_pos()`: clears stale RX bytes, drives `SEL` HIGH (transmit) with a 70µs settle, sends a single-byte read command (`uart.write(ID)`), waits up to 150µs for a 2-byte response, then decodes 14 bits of `transmission` down to a 12-bit position (`// we are using 12 bit encoder, datasheet says to throw out lowest two bits`), and validates an odd-parity checksum in the top 2 bits (comment: `// highest bit is for odd-numbered bits, second highest is for even / checksums calculated using odd parity`). Always leaves `SEL` LOW (idle/receive) before returning. `read_pos()` retries up to `max_tries`, normalizing the raw reading to `[0.0, 1.0]` (`0.0 -> 0 degrees, ..., 1.0 -> 360 degrees`). - -Top-of-file TODO: `// TODO - audit code below and update for EC, optionally break out RS485 funcs` — plus dead ISR-based code and a comment (`// flush doesn't do anything on portenta h7 but maybe on other platforms it will`) suggesting this was ported from a prior "Portenta" board and hasn't been fully validated on TOAD hardware yet. - -### `MksServo57D.h` / `.cpp` - -CAN driver for a Makerbase MKS SERVO57D closed-loop stepper driver, used to actuate throttle valve motors. Cites the datasheet and a reference implementation directly in comments. - -```cpp -class MksServo57D { -public: - MksServo57D(uint16_t can_id) : can_id(can_id) {}; - void begin(); - void set_speed(int16_t speed, uint8_t acceleration = 32); -private: - template std::array to_be_bytes(T value); - template void send_frame(uint8_t cmd, const std::array &data); - uint16_t can_id; -}; -``` - -`send_frame()` builds a CAN 2.0 frame `[cmd, ...data, crc]` where `crc = (can_id + cmd + sum(data)) & 0xFF`, but **never actually transmits it** — the function ends with `// TODO - transmit the frame` and does nothing further. **This means `set_speed()` currently has no real hardware effect** — it's fully unconnected to any CAN peripheral driver. `set_speed()` itself clamps `|speed|` to `[0, 400]` RPM (`// Max speed in open loop mode is 400 RPM.`) and packs direction into the top bit. - -### `ThrottleValves.h` / `.cpp` - -Combines one `MksServo57D` (motor) + one `AMT242AV` (encoder) per throttle valve (ox and fuel). - -```cpp -class ThrottleValve { -public: - ThrottleValve(uint16_t motor_can_id, Uart &enc_uart, unsigned int enc_SEL, unsigned int enc_ID); - void begin(); - void stop(); - void set_position(float angle); -private: - MksServo57D motor; - AMT242AV encoder; -}; - -namespace ThrottleValves { - bool begin(); - void stop(); - void set_angles_ox_fu(float ox_angle, float fu_angle); -} -``` - -`set_position()` is a bare proportional controller (`// TODO - PID controller logic`): `target_speed = (angle - current_angle) * K` with `float K = 1; // TODO - set this constant` — an unset/untuned P-only gain, no I or D term. Also flagged: `// TODO - if this turns the motor on, make sure we don't leave it on by mistake! could use heartbeat to solve`. **`ThrottleValves::begin()` currently just `// TODO - don't just return true here!`** — no real health check, so unlike `PressureSensors`/`TemperatureSensors`, this module can never fail the `all_modules_ok` gate in `ec_main.cpp`. - ---- - -## 13. `lib/tvc_actuators/` - -### `GimbalKinematics.h` / `.cpp` - -Pure-math library converting a commanded pitch/yaw pair into the two linear actuator extension lengths needed to achieve it, via 3D point rotation. - -```cpp -void calc_actuator_lengths(float primary_angle, float secondary_angle, float *primary_length, float *secondary_length); -``` - -Geometry constants (all marked `// TODO - update these`): -```cpp -#define BASE_POINT_DIST_FROM_ORIGIN_H 6.25 -#define BASE_POINT_DIST_FROM_ORIGIN_V -2 -#define ENGINE_POINT_DIST_FROM_ORIGIN_H 3.475 -#define ENGINE_POINT_DIST_FROM_ORIGIN_V 10.35 -#define BASE_ACTUATOR_LEN 12.6 -``` - -Rotates 4 fixed 3D attachment points by pitch then yaw and computes each actuator's required length as a *delta* from its neutral length (`// output lengths in inches (already subtracted from the base actuator length)`). Standalone and currently unverified — placeholder geometry constants mean computed lengths aren't trustworthy until measured on real hardware. - -### `TVC_Actuators.h` / `.cpp` - -```cpp -namespace TVC_Actuators { - bool begin(); - void set_angles_pitch_yaw(float pitch, float yaw); -} -``` - -`begin()` just `return true;`. `set_angles_pitch_yaw()` calls `calc_actuator_lengths()` but then **does nothing with the result** (`// TODO - this function`) — the actual actuator-commanding logic (presumably over CAN to `CAN_ID_TVC_PITCH`/`CAN_ID_TVC_YAW`) hasn't been written yet. This is the least-implemented module in the tree — a pure stub. - ---- - -## 14. `lib/valve_controller/ValveController.h` / `.cpp` - -Intended home for the EC's closed-loop throttle control algorithm — meant to take live PT/TC readings and compute target throttle-valve angles. Currently a stub. - -```cpp -struct valve_controller_output_t { - float ox_angle; // deg - float fu_angle; // deg -}; - -namespace ValveController { - bool begin(); - valve_controller_output_t get_controller_output(pressure_readings_t pt_readings, temperature_readings_t tc_readings); -} -``` - -`get_controller_output()` (`// TODO - this function`) always returns `{0.0, 0.0}` regardless of input — the core engine-mixture/thrust control loop that `ec_main.cpp`'s `flight_loop()` calls every iteration is entirely a placeholder today. - ---- - -## 15. The sensor/actuator abstraction pattern - -There's **no shared C++ interface/base class** — no virtual `ISensor`/`IValve`. Instead the codebase follows a consistent two-layer, hand-rolled convention repeated per subsystem: - -1. **Low-level driver class** (one per physical chip): `ADS131M02`, `Adafruit_MAX31856`, `AMT242AV`, `MksServo57D`. Each wraps exactly one device, takes its bus/pins in the constructor, exposes `begin()` plus device-specific methods. -2. **Application-level `namespace` singleton** (one per logical subsystem, matching the `ec_*` hardware_mapping headers): `PressureSensors`, `TemperatureSensors`, `SolenoidValves`, `ThrottleValves`, `TVC_Actuators`, `ValveController`. Each owns statically-allocated driver instances for every physical unit, and follows a uniform naming convention: - - `bool begin()` — inits all owned hardware, returns overall success. Consumed by `ec_main.cpp`'s `all_modules_ok &= X::begin();` gate. **Note**: today this gate is only meaningfully enforced by `PressureSensors` and `TemperatureSensors` — `ThrottleValves`, `TVC_Actuators`, and `ValveController` all just `return true`. - - a `read_*()`/`get_*()` returning a plain result struct (`pressure_readings_t`, `temperature_readings_t`, `valve_controller_output_t`) — these structs, defined centrally in `ec_sensors.h`/`ValveController.h`, are the shared data-interchange format between subsystems. - - a `set_*`/action function for actuators (`set_angles_ox_fu`, `set_angles_pitch_yaw`, `update_rcs_valves`, `set_valves_from_valve_state`). - - most register at least one `CommandRouter::add(...)` CLI command for interactive debugging (`print_pt`, `print_tc`, `open_valve`/`close_valve`/`sv_ui`). - -A middle "per-unit" class recurs for multi-channel devices — `PT_Board` (2 PTs sharing 1 ADC) and `ThrottleValve` (1 motor + 1 encoder) — bundling multiple driver instances that logically belong together, instantiated as fixed-size arrays/named globals inside the owning namespace's `.cpp`. - -This keeps each module simple but means some duplication (every SPI sensor driver independently defines its own `SPISettings`, every namespace writes its own `begin()`-failure printf loop) — a candidate for a future shared-interface refactor if you're looking for one. - ---- - -## 16. How CAN, RS485, and Serial fit together - -- **USB / primary serial (`HW_CommsSerial`, aliased `CommsSerial`) and fallback serial (`HW_FallbackSerial`)** — the human/GSE-facing text CLI, driven by `CommandRouter`. Used to arm/kill the flight loop, open/close valves manually, read live sensor values. **Not** part of the real-time flight-critical path — polled non-blockingly once per `loop()`/`flight_loop()` iteration. -- **RS485 (`RS485_6`, `RS485_2`, plain `Uart` + DE pin)** — half-duplex point-to-point links used specifically for the `AMT242AV` absolute encoders (plus reserved-but-unused `DRV_OX_RS485_BUS`/`DRV_FU_RS485_BUS` slots for driver comms). This is the *sensor feedback* bus for throttle-valve position and TVC actuator selection, addressed via per-device `SEL` GPIOs so multiple devices can share one physical bus. -- **CAN (FDCAN peripherals, `toad_can_bus.h`)** — the command/actuation bus. Connects EC to the `MksServo57D` stepper drivers and TVC actuators on the "engine control CAN bus" (CAN 2.0), and separately EC↔FC↔power-board on the "primary vehicle CAN bus" (CAN-FD) and GSE↔programmer-boards on the "GSE CAN bus" (CAN-FD). `CAN_Msg_Decoder` standardizes parsing incoming frames against a state machine — used concretely today in `prog_main.cpp`'s bootloader protocol. - -**Gap to know about**: the actual CAN *transmit* path — whether from `MksServo57D::send_frame()` or from `CAN_Msg_Decoder`'s error-reply TODOs — isn't implemented anywhere yet. The CAN layer is currently receive/decode-only in terms of what's actually wired up, even though the send-side framing logic already exists. - -**In short**: serial = human interactive control/telemetry · RS485 = local sensor feedback (encoders) · CAN = inter-board command/actuation and cross-controller telemetry/bootloading. Three physically and functionally distinct layers. - ---- - -## 17. Where is `HardwareSerial` / `Uart` defined? - -You asked specifically about this — here's the story. `HardwareSerial`/`Uart` are **not part of this repo**; they come from the STM32duino Arduino core, bundled inside the PlatformIO `ststm32` platform package (`framework-arduinoststm32`). That package hasn't been downloaded in this checkout (no `.pio` build dir yet, meaning the project hasn't been built here) — so there's no local file path to point you to until you run a build (`pio run -e engine_controller`), which will fetch it. - -Once fetched, it'll land under something like: -``` -~/.platformio/packages/framework-arduinoststm32/cores/arduino/HardwareSerial.h -~/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32/uart.c (C HAL wrapper) -~/.platformio/packages/framework-arduinoststm32/libraries/SrcWrapper/src/stm32/uart.cpp -``` -(not independently verified against this checkout — verify once you've built). - -**Why `Uart` and not `HardwareSerial`?** This repo's own git history explains it — three relevant commits: -``` -84d8c1b switch to ststm32 platform version 20.0.0 change all uses of HardwareSerial to Uart due to breaking change on stm32duino version 3.0.0 -bf39458 pin platform due to breaking HardwareSerial changes -d8a0580 make the linker error go away that is caused by missing _write definition -``` -STM32duino's core made a breaking change in v3.0.0: `HardwareSerial` became the abstract/base API class (inherited from ArduinoCore-API), and `Uart` became STM32's concrete subclass that application code should instantiate directly. `platformio.ini` pins `platform = ststm32 @ 20.0.0` specifically to lock in this version of the core. Every UART object in this codebase — `ec_main.cpp`'s `HW_CommsSerial`, `HW_FallbackSerial`, `RS485_6`, `RS485_2`; `ec_pins.h`'s `extern Uart RS485_6;`; `AMT242AV`'s `Uart &uart` constructor arg — consistently uses `Uart`, confirming the migration was applied throughout. - ---- - -## 18. Known stubs / TODOs / bugs worth knowing before you dive in - -A consolidated list, so you don't have to rediscover these: - -| Location | Issue | -|---|---| -| `lib/RS485/RS485.h` | Empty stub class, missing terminating `;`, not referenced anywhere. | -| `lib/tvc_actuators/TVC_Actuators.cpp` | `set_angles_pitch_yaw()` computes lengths but does nothing with them — pure stub. | -| `lib/valve_controller/ValveController.cpp` | `get_controller_output()` always returns `{0, 0}` — the core throttle control loop is unimplemented. | -| `lib/throttle_valves/MksServo57D.cpp` | `send_frame()` builds the CAN frame but never transmits it — `set_speed()` has no hardware effect. | -| `lib/throttle_valves/ThrottleValves.cpp` | `begin()` always returns `true` — no real health check, unlike PressureSensors/TemperatureSensors. | -| `lib/throttle_valves/ThrottleValves.cpp` | `set_position()` is P-only control with an untuned `K = 1`; no PID yet. | -| `lib/can_bus/toad_can_bus.h` | `CAN_Msg_Decoder`'s error-reply paths are all `// TODO - send` — no CAN transmit path implemented anywhere. | -| `lib/temperature_sensors/Adafruit_MAX31856.cpp` | Chip-select toggled via `pinMode(cs_pin, HIGH/LOW)` instead of `digitalWrite()` — likely a bug carried from upstream Adafruit code. | -| `lib/temperature_sensors/TemperatureSensors.cpp` | `print_tc_readings()` prints Fahrenheit values but labels them `C` in the format string. | -| `lib/pressure_sensors/pt_calibration.h` | All PT calibration constants are placeholders (`slope=0, offset=10`). | -| `lib/pressure_sensors/PressureSensors.cpp` | Raw ADC→engineering-units conversion flagged as incomplete (missing voltage-level/2²⁴ scaling). | -| `lib/tvc_actuators/GimbalKinematics.cpp` | All actuator geometry constants marked `// TODO - update these` — computed lengths not yet trustworthy. | -| `src/prog_main.cpp` | CAN RX payload parsing (`raw_bytes`/`raw_msg_len`) never actually populated; likely `chunk_rcv[i]` vs `chunk_rcv` array-truthiness bug in the page-write state machine. | -| `lib/throttle_valves/AMT242AV.cpp` | Header flags `// TODO - audit code below and update for EC` — ported from a prior "Portenta" board, not fully validated on TOAD hardware. | -| `src/fc_main.cpp` | Entire FC firmware is a two-line stub. | - ---- - -*Generated as an orientation aid — verify against the current source before relying on specifics, since this is a fast-moving embedded codebase and several of the "stub"/"TODO" items above are exactly the kind of thing that gets fixed without this doc being updated.* From 3322e798d5e6a017ff63b38f063e913d4c9cd79b Mon Sep 17 00:00:00 2001 From: Rishab Date: Thu, 17 Sep 2026 09:21:33 -0400 Subject: [PATCH 03/10] added third RS485 to feature implementation --- firmware/lib/RS485/RS485.cpp | 9 +++++++-- firmware/lib/RS485/RS485.h | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/firmware/lib/RS485/RS485.cpp b/firmware/lib/RS485/RS485.cpp index b9b7e68..bcf58d8 100644 --- a/firmware/lib/RS485/RS485.cpp +++ b/firmware/lib/RS485/RS485.cpp @@ -143,10 +143,12 @@ namespace RS485s { namespace { constexpr uint32_t kEncBaud = 2000000; // AMT24 2 Mbps data rate constexpr uint32_t kTvcBaud = 2000000; // TODO - Check Baud rate for TVC +constexpr uint32_t kDrvBaud = 115200; // TODO - check driver's RS485 config; placeholder + // Index in each array == device index below -const uint32_t bus6_sels[] = {PIN_TVC_PITCH_SEL, PIN_ENC_OX_SEL}; -const uint32_t bus2_sels[] = {PIN_TVC_YAW_SEL, PIN_ENC_FU_SEL}; +const uint32_t bus6_sels[] = {PIN_TVC_PITCH_SEL, PIN_ENC_OX_SEL, PIN_DRV_OX_SEL}; +const uint32_t bus2_sels[] = {PIN_TVC_YAW_SEL, PIN_ENC_FU_SEL, PIN_DRV_FU_SEL}; } // namespace RS485Bus bus6(RS485_6, bus6_sels, std::size(bus6_sels)); @@ -154,8 +156,11 @@ RS485Bus bus2(RS485_2, bus2_sels, std::size(bus2_sels)); RS485Device tvc_pitch(bus6, 0, kTvcBaud); RS485Device enc_ox(bus6, 1, kEncBaud); +RS485Device drv_ox(bus6, 2, kDrvBaud); + RS485Device tvc_yaw(bus2, 0, kTvcBaud); RS485Device enc_fu(bus2, 1, kEncBaud); +RS485Device drv_fu(bus2, 2, kDrvBaud); bool begin() { bool ok = true; diff --git a/firmware/lib/RS485/RS485.h b/firmware/lib/RS485/RS485.h index 192ff24..f241407 100644 --- a/firmware/lib/RS485/RS485.h +++ b/firmware/lib/RS485/RS485.h @@ -64,6 +64,8 @@ extern RS485Bus bus2; extern RS485Device tvc_pitch; extern RS485Device enc_ox; +extern RS485Device drv_ox; extern RS485Device tvc_yaw; extern RS485Device enc_fu; +extern RS485Device drv_fu; } // namespace RS485s \ No newline at end of file From c43c2b02938e2fc83e78fd3fc5c3368e245d0502 Mon Sep 17 00:00:00 2001 From: Rishab Date: Fri, 18 Sep 2026 00:44:12 -0400 Subject: [PATCH 04/10] fixed a bug with the rs485 read(). Actually waits until TX finishes before read attempt --- firmware/lib/RS485/RS485.cpp | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/firmware/lib/RS485/RS485.cpp b/firmware/lib/RS485/RS485.cpp index bcf58d8..02985b2 100644 --- a/firmware/lib/RS485/RS485.cpp +++ b/firmware/lib/RS485/RS485.cpp @@ -4,7 +4,7 @@ #include namespace { -constexpr uint32_t kMarginUs = 200; // tune against response time +constexpr uint32_t kMarginUs = 200; // TODO: tune against response time constexpr uint32_t kTxSlackUs = 2000; } // namespace @@ -61,7 +61,7 @@ void RS485Bus::applyDE(USART_TypeDef *u) { u->CR3 |= USART_CR3_DEM; u->CR1 &= ~(USART_CR1_DEAT | USART_CR1_DEDT); - // DEAT = 31/16 bit: gives the transceiver time to enable + // DEAT = 31/16 bit: gives the transceiver time to enable u->CR1 |= (31U << USART_CR1_DEAT_Pos) | (1U << USART_CR1_DEDT_Pos); } @@ -116,20 +116,23 @@ bool RS485Device::beginTransaction() { size_t RS485Device::read(uint8_t *dst, size_t len, uint32_t latency_us) { // Don't start response clock while our own request is still sending - bus_.waitTxComplete(bus_.frameTimeUs(SERIAL_TX_BUFFER_SIZE) + kTxSlackUs); + bool complete = bus_.waitTxComplete(bus_.frameTimeUs(SERIAL_TX_BUFFER_SIZE) + kTxSlackUs); const uint32_t budget = latency_us + bus_.frameTimeUs(len) + kMarginUs; const uint32_t start = micros(); size_t n = 0; - while (n < len) { - if (uart.available()) { - dst[n++] = (uint8_t)uart.read(); - continue; + if (complete) { + while (n < len) { + if (uart.available()) { + dst[n++] = (uint8_t)uart.read(); + continue; + } + if (micros() - start >= budget) + break; } - if (micros() - start >= budget) - break; + return n; } - return n; + return 0; } void RS485Device::endTransaction() { @@ -137,14 +140,13 @@ void RS485Device::endTransaction() { bus_.deselectAll(); } -// Namespace Globals +// Namespace Globals namespace RS485s { namespace { constexpr uint32_t kEncBaud = 2000000; // AMT24 2 Mbps data rate constexpr uint32_t kTvcBaud = 2000000; // TODO - Check Baud rate for TVC -constexpr uint32_t kDrvBaud = 115200; // TODO - check driver's RS485 config; placeholder - +constexpr uint32_t kDrvBaud = 115200; // TODO - check driver's RS485 config; placeholder // Index in each array == device index below const uint32_t bus6_sels[] = {PIN_TVC_PITCH_SEL, PIN_ENC_OX_SEL, PIN_DRV_OX_SEL}; From a418251b49659b3a588ba38c2e50978ee37e7cc6 Mon Sep 17 00:00:00 2001 From: Rishab Date: Sun, 20 Sep 2026 00:25:36 -0400 Subject: [PATCH 05/10] Adressed Robert's Comments about using .begin() and HAL and removed setBaud() critical section --- firmware/lib/RS485/RS485.cpp | 57 ++++++++++++------------- firmware/lib/RS485/RS485.h | 14 +++--- firmware/lib/hardware_mapping/ec_pins.h | 21 +++++---- firmware/src/ec_main.cpp | 4 -- 4 files changed, 46 insertions(+), 50 deletions(-) diff --git a/firmware/lib/RS485/RS485.cpp b/firmware/lib/RS485/RS485.cpp index 02985b2..4adbd29 100644 --- a/firmware/lib/RS485/RS485.cpp +++ b/firmware/lib/RS485/RS485.cpp @@ -6,21 +6,19 @@ namespace { constexpr uint32_t kMarginUs = 200; // TODO: tune against response time constexpr uint32_t kTxSlackUs = 2000; +constexpr uint32_t kAckTimeoutUs = 1000; // TEACK/REACK after UE re-enable + } // namespace -RS485Bus::RS485Bus(Uart &uart, const uint32_t *sels, size_t sel_count) - : uart_(uart), sels_(sels), sel_count_(sel_count) {} +RS485Bus::RS485Bus(uint32_t rx, uint32_t tx, uint32_t de, const uint32_t *sels, size_t sel_count) + : uart_(rx, tx, de), sels_(sels), sel_count_(sel_count) {} bool RS485Bus::begin(uint32_t baud) { for (size_t i = 0; i < sel_count_; i++) { pinMode(sels_[i], OUTPUT); digitalWrite(sels_[i], LOW); } - - uart_.begin(baud); // core muxes RX/TX/DE (as RTS) and sets RTSE - if (!uart_) - return false; - return setBaud(baud); // also converts RTS flow control -> DE mode + return initAt(baud); } bool RS485Bus::setBaud(uint32_t baud) { @@ -28,32 +26,31 @@ bool RS485Bus::setBaud(uint32_t baud) { return false; if (!waitTxComplete(frameTimeUs(SERIAL_TX_BUFFER_SIZE) + kTxSlackUs)) return false; + uart_.end(); + return initAt(baud); +} - UART_HandleTypeDef *h = uart_.getHandle(); - USART_TypeDef *u = h->Instance; - - h->Init.BaudRate = baud; - h->Init.HwFlowCtl = UART_HWCONTROL_NONE; // stop UART_SetConfig from re-enabling RTSE + // RS485 delta from HAL_RS485Ex_Init. SetConfig/AdvFeatureConfig + // are already done by uart_.begin(). UART_CheckIdleState skipped because it resets RxState and + // would disarm the core's Receive_IT; TX idle, bus deselected. - noInterrupts(); - // UART_SetConfig clears the HAL ISR pointers the core's IT-mode RX depends on. - auto rx_isr = h->RxISR; - auto tx_isr = h->TxISR; +bool RS485Bus::initAt(uint32_t baud) { + uart_.begin(baud); - u->CR1 &= ~USART_CR1_UE; // BRR/DE fields writable only with UE = 0 - bool ok = (UART_SetConfig(h) == HAL_OK); // BRR + PRESC from the real clock source + USART_TypeDef *u = uart_.getHandle()->Instance; + u->CR1 &= ~USART_CR1_UE; applyDE(u); u->CR1 |= USART_CR1_UE; - - h->RxISR = rx_isr; - h->TxISR = tx_isr; interrupts(); - while (uart_.available()) - uart_.read(); // anything framed at the old rate is garbage - if (ok) - baud_ = baud; - return ok; + const uint32_t ack = USART_ISR_TEACK | USART_ISR_REACK; + const uint32_t start = micros(); + while ((u->ISR & ack) != ack) { + if (micros() - start >= kAckTimeoutUs) + return false; + } + baud_ = baud; + return true; } void RS485Bus::applyDE(USART_TypeDef *u) { @@ -65,7 +62,7 @@ void RS485Bus::applyDE(USART_TypeDef *u) { u->CR1 |= (31U << USART_CR1_DEAT_Pos) | (1U << USART_CR1_DEDT_Pos); } -bool RS485Bus::deModeActive() const { +bool RS485Bus::deModeActive() { return (uart_.getHandle()->Instance->CR3 & USART_CR3_DEM) != 0; } @@ -153,8 +150,8 @@ const uint32_t bus6_sels[] = {PIN_TVC_PITCH_SEL, PIN_ENC_OX_SEL, PIN_DRV_OX_SEL} const uint32_t bus2_sels[] = {PIN_TVC_YAW_SEL, PIN_ENC_FU_SEL, PIN_DRV_FU_SEL}; } // namespace -RS485Bus bus6(RS485_6, bus6_sels, std::size(bus6_sels)); -RS485Bus bus2(RS485_2, bus2_sels, std::size(bus2_sels)); +RS485Bus bus6(PIN_RS485_6_RX, PIN_RS485_6_TX, PIN_RS485_6_DE, bus6_sels, std::size(bus6_sels)); +RS485Bus bus2(PIN_RS485_2_RX, PIN_RS485_2_TX, PIN_RS485_2_DE, bus2_sels, std::size(bus2_sels)); RS485Device tvc_pitch(bus6, 0, kTvcBaud); RS485Device enc_ox(bus6, 1, kEncBaud); @@ -166,7 +163,7 @@ RS485Device drv_fu(bus2, 2, kDrvBaud); bool begin() { bool ok = true; - ok &= bus6.begin(kEncBaud); // start at the in-flight rate + ok &= bus6.begin(kEncBaud); ok &= bus2.begin(kEncBaud); ok &= bus6.deModeActive(); ok &= bus2.deModeActive(); diff --git a/firmware/lib/RS485/RS485.h b/firmware/lib/RS485/RS485.h index f241407..d141fd3 100644 --- a/firmware/lib/RS485/RS485.h +++ b/firmware/lib/RS485/RS485.h @@ -6,22 +6,25 @@ // DE pin is passed to the Uart constructor as RTS so that the core muxes it, // and RS485Bus converts RTS flow control into DE mode. -// NOTE: DO NOT call uart.begin() on a bus after RS485s::begin() since it will re-enable RTSE and kill DE. +// NOTE: (re)initialize only via RS485Bus::begin()/setBaud(). Calling uart().begin() +// directly re-enables RTSE and drops DE mode. class RS485Bus { public: - RS485Bus(Uart &uart, const uint32_t *sels, size_t sel_count); + RS485Bus(uint32_t rx, uint32_t tx, uint32_t de, const uint32_t *sels, size_t sel_count); - bool begin(uint32_t baud); // once, at startup + bool begin(const uint32_t baud); // once, at startup bool setBaud(uint32_t baud); // waits for TX to finish uint32_t baud() const { return baud_; } + Uart &uart() { return uart_; } + const Uart &uart() const { return uart_; } void deselectAll(); bool waitTxComplete(uint32_t timeout_us); uint32_t frameTimeUs(size_t len) const; // 8N1: 10 bits per byte - bool deModeActive() const; + bool deModeActive(); private: - Uart &uart_; + Uart uart_; const uint32_t *sels_; size_t sel_count_; uint32_t baud_ = 0; @@ -30,6 +33,7 @@ class RS485Bus { static void applyDE(USART_TypeDef *u); // requires UE = 0 friend class RS485Device; + bool initAt(uint32_t baud); // core begin() + RS485 delta }; class RS485Device { diff --git a/firmware/lib/hardware_mapping/ec_pins.h b/firmware/lib/hardware_mapping/ec_pins.h index 9eb8cc9..03dcee0 100644 --- a/firmware/lib/hardware_mapping/ec_pins.h +++ b/firmware/lib/hardware_mapping/ec_pins.h @@ -2,6 +2,7 @@ #include +#include "RS485.h" #include "SPI.h" // UARTS @@ -14,9 +15,7 @@ #define PIN_HW_FALLBACK_SERIAL_TX PB10 // RS485 Busses (UART6 and UART2) -// these are declared in ec_main -extern Uart RS485_6; // RS485 on UART 6 -extern Uart RS485_2; // RS485 on UART 2 +// These UARTs are owned by the RS485Bus objects in RS485.cpp. #define PIN_RS485_6_RX PG9 #define PIN_RS485_6_TX PG14 @@ -25,18 +24,18 @@ extern Uart RS485_2; // RS485 on UART 2 #define PIN_RS485_2_TX PD5 #define PIN_RS485_2_DE PD4 -#define TVC_PITCH_RS485_BUS RS485_6 +#define TVC_PITCH_RS485_BUS RS485s::bus6 #define PIN_TVC_PITCH_SEL PF8 -#define DRV_OX_RS485_BUS RS485_6 // unused -#define PIN_DRV_OX_SEL PF9 // unused -#define ENC_OX_RS485_BUS RS485_6 +#define DRV_OX_RS485_BUS RS485s::bus6 // unused +#define PIN_DRV_OX_SEL PF9 // unused +#define ENC_OX_RS485_BUS RS485s::bus6.uart() #define PIN_ENC_OX_SEL PF10 -#define TVC_YAW_RS485_BUS RS485_2 +#define TVC_YAW_RS485_BUS RS485s::bus2 #define PIN_TVC_YAW_SEL PD8 -#define DRV_FU_RS485_BUS RS485_2 // unused -#define PIN_DRV_FU_SEL PD9 // unused -#define ENC_FU_RS485_BUS RS485_2 +#define DRV_FU_RS485_BUS RS485s::bus2 // unused +#define PIN_DRV_FU_SEL PD9 // unused +#define ENC_FU_RS485_BUS RS485s::bus2.uart() #define PIN_ENC_FU_SEL PD10 // SPI diff --git a/firmware/src/ec_main.cpp b/firmware/src/ec_main.cpp index 2f24cb0..7ef735e 100644 --- a/firmware/src/ec_main.cpp +++ b/firmware/src/ec_main.cpp @@ -18,10 +18,6 @@ CommsSerial_t USB_CommsSerial; CommsSerial_t HW_CommsSerial(PIN_HW_COMM_SERIAL_RX, PIN_HW_COMM_SERIAL_TX); CommsSerial_t HW_FallbackSerial(PIN_HW_FALLBACK_SERIAL_RX, PIN_HW_FALLBACK_SERIAL_TX); -// DE is passed as RTS and is converted to hardware DE mode by RS485Bus; DO NOT call begin() on these. -Uart RS485_6(PIN_RS485_6_RX, PIN_RS485_6_TX, PIN_RS485_6_DE); -Uart RS485_2(PIN_RS485_2_RX, PIN_RS485_2_TX, PIN_RS485_2_DE); - SPIClass PT_TC_SPI_1(PIN_PT_TC_SPI_1_MOSI, PIN_PT_TC_SPI_1_MISO, PIN_PT_TC_SPI_1_SCK); SPIClass PT_TC_SPI_3(PIN_PT_TC_SPI_3_MOSI, PIN_PT_TC_SPI_3_MISO, PIN_PT_TC_SPI_3_SCK); From 59f3d2038b1667570874a75fff8a2caee6c813d1 Mon Sep 17 00:00:00 2001 From: Rishab Date: Sat, 26 Sep 2026 11:36:53 -0400 Subject: [PATCH 06/10] Saving a little outdated, but seemingly correct code --- firmware/lib/RS485/RS485.cpp | 46 +++++++++++++++---------- firmware/lib/RS485/RS485.h | 5 +-- firmware/lib/hardware_mapping/ec_pins.h | 4 +-- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/firmware/lib/RS485/RS485.cpp b/firmware/lib/RS485/RS485.cpp index 4adbd29..2784acf 100644 --- a/firmware/lib/RS485/RS485.cpp +++ b/firmware/lib/RS485/RS485.cpp @@ -1,7 +1,8 @@ #include "RS485.h" #include "CommsSerial.h" #include "ec_pins.h" -#include +#include // used to calculate size of the array hosuing the select pins in RS485s namespace. +// TO-DO: Restructure to use std::array and remove sel_count parameter namespace { constexpr uint32_t kMarginUs = 200; // TODO: tune against response time @@ -30,9 +31,9 @@ bool RS485Bus::setBaud(uint32_t baud) { return initAt(baud); } - // RS485 delta from HAL_RS485Ex_Init. SetConfig/AdvFeatureConfig - // are already done by uart_.begin(). UART_CheckIdleState skipped because it resets RxState and - // would disarm the core's Receive_IT; TX idle, bus deselected. +// RS485 delta from HAL_RS485Ex_Init. SetConfig/AdvFeatureConfig +// are already done by uart_.begin(). UART_CheckIdleState skipped because it resets RxState and +// would disarm the core's Receive_IT; TX idle, bus deselected. bool RS485Bus::initAt(uint32_t baud) { uart_.begin(baud); @@ -41,7 +42,6 @@ bool RS485Bus::initAt(uint32_t baud) { u->CR1 &= ~USART_CR1_UE; applyDE(u); u->CR1 |= USART_CR1_UE; - interrupts(); const uint32_t ack = USART_ISR_TEACK | USART_ISR_REACK; const uint32_t start = micros(); @@ -54,7 +54,7 @@ bool RS485Bus::initAt(uint32_t baud) { } void RS485Bus::applyDE(USART_TypeDef *u) { - u->CR3 &= ~(USART_CR3_RTSE | USART_CR3_DEP); // no RTS flow control, DE active-high + u->CR3 &= ~(USART_CR3_RTSE | USART_CR3_DEP); // no RTS flow control, DE active-high // call pinmap_pinout() u->CR3 |= USART_CR3_DEM; u->CR1 &= ~(USART_CR1_DEAT | USART_CR1_DEDT); @@ -79,13 +79,18 @@ void RS485Bus::select(size_t idx) { } } -// Same condition Uart::flush() waits on (tx_tail advances in the TC callback), but bounded. +// Wait for the hardware TX complete flag, not just the software TX buffer depth. +// previously used availableForWrite(), but that only tells us the queue has room, not that the final byte +// has left the shift register and the bus is safe to deselect. bool RS485Bus::waitTxComplete(uint32_t timeout_us) { + USART_TypeDef *u = uart_.getHandle()->Instance; const uint32_t start = micros(); - while (uart_.availableForWrite() < SERIAL_TX_BUFFER_SIZE - 1) { + + while ((u->ISR & USART_ISR_TC) == 0) { if (micros() - start >= timeout_us) return false; } + return true; } @@ -115,21 +120,26 @@ size_t RS485Device::read(uint8_t *dst, size_t len, uint32_t latency_us) { // Don't start response clock while our own request is still sending bool complete = bus_.waitTxComplete(bus_.frameTimeUs(SERIAL_TX_BUFFER_SIZE) + kTxSlackUs); + if (!complete) { + return 0; // Error Code 0; Bus is still transmitting + } + const uint32_t budget = latency_us + bus_.frameTimeUs(len) + kMarginUs; const uint32_t start = micros(); size_t n = 0; - if (complete) { - while (n < len) { - if (uart.available()) { - dst[n++] = (uint8_t)uart.read(); - continue; - } - if (micros() - start >= budget) - break; + + while (n < len) { + if (uart.available()) { + dst[n++] = (uint8_t)uart.read(); + continue; // ensure all bytes are read before checking timeout + } + + if (micros() - start >= budget) { + break; } - return n; } - return 0; + + return n; } void RS485Device::endTransaction() { diff --git a/firmware/lib/RS485/RS485.h b/firmware/lib/RS485/RS485.h index d141fd3..0814ef2 100644 --- a/firmware/lib/RS485/RS485.h +++ b/firmware/lib/RS485/RS485.h @@ -22,9 +22,10 @@ class RS485Bus { bool waitTxComplete(uint32_t timeout_us); uint32_t frameTimeUs(size_t len) const; // 8N1: 10 bits per byte bool deModeActive(); - -private: Uart uart_; + +private: + const uint32_t *sels_; size_t sel_count_; uint32_t baud_ = 0; diff --git a/firmware/lib/hardware_mapping/ec_pins.h b/firmware/lib/hardware_mapping/ec_pins.h index 03dcee0..aa7cc50 100644 --- a/firmware/lib/hardware_mapping/ec_pins.h +++ b/firmware/lib/hardware_mapping/ec_pins.h @@ -28,14 +28,14 @@ #define PIN_TVC_PITCH_SEL PF8 #define DRV_OX_RS485_BUS RS485s::bus6 // unused #define PIN_DRV_OX_SEL PF9 // unused -#define ENC_OX_RS485_BUS RS485s::bus6.uart() +#define ENC_OX_RS485_BUS RS485s::bus6 #define PIN_ENC_OX_SEL PF10 #define TVC_YAW_RS485_BUS RS485s::bus2 #define PIN_TVC_YAW_SEL PD8 #define DRV_FU_RS485_BUS RS485s::bus2 // unused #define PIN_DRV_FU_SEL PD9 // unused -#define ENC_FU_RS485_BUS RS485s::bus2.uart() +#define ENC_FU_RS485_BUS RS485s::bus2 #define PIN_ENC_FU_SEL PD10 // SPI From 46e80355c8f014525ab2dc3b1dc9366ca79060da Mon Sep 17 00:00:00 2001 From: Rishab Date: Sat, 26 Sep 2026 13:17:04 -0400 Subject: [PATCH 07/10] Proper Implementation of RS485 with inheritance, and HAL --- firmware/lib/RS485/RS485.cpp | 203 ++++++++++++++++++++--------------- firmware/lib/RS485/RS485.h | 90 ++++++++++------ 2 files changed, 172 insertions(+), 121 deletions(-) diff --git a/firmware/lib/RS485/RS485.cpp b/firmware/lib/RS485/RS485.cpp index 2784acf..8d2d8f9 100644 --- a/firmware/lib/RS485/RS485.cpp +++ b/firmware/lib/RS485/RS485.cpp @@ -1,96 +1,117 @@ #include "RS485.h" #include "CommsSerial.h" +#include "PeripheralPins.h" #include "ec_pins.h" -#include // used to calculate size of the array hosuing the select pins in RS485s namespace. -// TO-DO: Restructure to use std::array and remove sel_count parameter +#include "pinmap.h" +#include "stm32yyxx_ll_usart.h" namespace { constexpr uint32_t kMarginUs = 200; // TODO: tune against response time constexpr uint32_t kTxSlackUs = 2000; -constexpr uint32_t kAckTimeoutUs = 1000; // TEACK/REACK after UE re-enable +// DE timing, in sample-time units (1/16 bit at OVER16); HAL range 0..31. +// Assertion: DE rises this long before the start bit. Must exceed the transceiver's +// driver-enable time (t_ZH / t_ZL in its datasheet). 31 = 1.94 bit = 0.97 us at 2 Mbps. +constexpr uint32_t kDeAssertTime = 31; +// Deassertion: DE held this long after the end of the last stop bit. +constexpr uint32_t kDeDeassertTime = 1; } // namespace -RS485Bus::RS485Bus(uint32_t rx, uint32_t tx, uint32_t de, const uint32_t *sels, size_t sel_count) - : uart_(rx, tx, de), sels_(sels), sel_count_(sel_count) {} +void RS485Bus::begin(unsigned long baud, uint16_t config) { + rs485_ok_ = false; -bool RS485Bus::begin(uint32_t baud) { + // Transceiver in receive and all devices deselected until the USART owns DE. + pinMode(de_, OUTPUT); + digitalWrite(de_, LOW); for (size_t i = 0; i < sel_count_; i++) { pinMode(sels_[i], OUTPUT); digitalWrite(sels_[i], LOW); } - return initAt(baud); + + Uart::begin(baud, config); // clocks, RX/TX mux, NVIC, HAL_UART_Init, RX armed + if (!Uart::operator bool() || config != SERIAL_8N1) // frameTimeUs() assumes 8N1 + return; + + rs485_ok_ = configureRS485(baud) && muxDE(); } -bool RS485Bus::setBaud(uint32_t baud) { - if (baud == 0) - return false; - if (!waitTxComplete(frameTimeUs(SERIAL_TX_BUFFER_SIZE) + kTxSlackUs)) - return false; - uart_.end(); - return initAt(baud); +void RS485Bus::end() { + rs485_ok_ = false; + Uart::end(); + // Take DE back from the now-unclocked USART and hold receive. + pinMode(de_, OUTPUT); + digitalWrite(de_, LOW); } -// RS485 delta from HAL_RS485Ex_Init. SetConfig/AdvFeatureConfig -// are already done by uart_.begin(). UART_CheckIdleState skipped because it resets RxState and -// would disarm the core's Receive_IT; TX idle, bus deselected. +RS485Bus::operator bool() { + // rs485_ok_ first: before begin() the handle's Instance is null. + return rs485_ok_ && Uart::operator bool() && LL_USART_IsEnabledDEMode(getHandle()->Instance); +} -bool RS485Bus::initAt(uint32_t baud) { - uart_.begin(baud); +// Reconfigure the core's own handle for RS485, in place. +// HAL_RS485Ex_Init re-runs UART_SetConfig and UART_CheckIdleState; the latter sets +// RxState = READY, which orphans the core's pending 1-byte Receive_IT (the ISR would +// then discard every byte). So: stop RX explicitly, reconfigure, re-arm RX through +// the core's own entry point, and verify it is armed. +// +// NOTE: UART_CheckIdleState waits for TEACK with HAL_UART_TIMEOUT_VALUE (~9 h), not a +// short timeout. It only blocks if the USART kernel clock is dead; the core's boot-time +// HAL_UART_Init has the same exposure. +bool RS485Bus::configureRS485(uint32_t baud) { + UART_HandleTypeDef *h = getHandle(); + + HAL_NVIC_DisableIRQ(_serial.irq); // core does the same around Receive_IT (handle lock) + HAL_UART_AbortReceive(h); + h->Init.BaudRate = baud; + h->Init.HwFlowCtl = UART_HWCONTROL_NONE; // the pin is DE, not RTS + const bool ok = HAL_RS485Ex_Init(h, UART_DE_POLARITY_HIGH, kDeAssertTime, kDeDeassertTime) == HAL_OK; + HAL_NVIC_EnableIRQ(_serial.irq); + if (!ok) + return false; - USART_TypeDef *u = uart_.getHandle()->Instance; - u->CR1 &= ~USART_CR1_UE; - applyDE(u); - u->CR1 |= USART_CR1_UE; + uart_attach_rx_callback(&_serial, _rx_complete_irq); + if ((HAL_UART_GetState(h) & HAL_UART_STATE_BUSY_RX) != HAL_UART_STATE_BUSY_RX) + return false; // RX not re-armed: fail closed - const uint32_t ack = USART_ISR_TEACK | USART_ISR_REACK; - const uint32_t start = micros(); - while ((u->ISR & ack) != ack) { - if (micros() - start >= kAckTimeoutUs) - return false; - } baud_ = baud; return true; } -void RS485Bus::applyDE(USART_TypeDef *u) { - u->CR3 &= ~(USART_CR3_RTSE | USART_CR3_DEP); // no RTS flow control, DE active-high // call pinmap_pinout() - u->CR3 |= USART_CR3_DEM; - u->CR1 &= ~(USART_CR1_DEAT | USART_CR1_DEDT); - - // DEAT = 31/16 bit: gives the transceiver time to enable - u->CR1 |= (31U << USART_CR1_DEAT_Pos) | (1U << USART_CR1_DEDT_Pos); -} - -bool RS485Bus::deModeActive() { - return (uart_.getHandle()->Instance->CR3 & USART_CR3_DEM) != 0; -} - -void RS485Bus::deselectAll() { - for (size_t i = 0; i < sel_count_; i++) { - digitalWrite(sels_[i], LOW); - } +// Mux DE with the same call the core uses for RX/TX/RTS. RTS and DE share one AF on +// STM32, so the RTS pinmap is the DE pinmap. The peripheral is checked first because +// pinmap_pinout() hangs in Error_Handler() on an unmapped pin, and because lookup is +// first-match: if this pin's DE function is on an _ALTx entry, fail loudly here rather +// than mux the wrong AF. +bool RS485Bus::muxDE() { + const PinName pn = digitalPinToPinName(de_); + if (pinmap_peripheral(pn, PinMap_UART_RTS) != (void *)getHandle()->Instance) + return false; + pinmap_pinout(pn, PinMap_UART_RTS); + return true; } -void RS485Bus::select(size_t idx) { - deselectAll(); - if (idx < sel_count_) { - digitalWrite(sels_[idx], HIGH); - } +bool RS485Bus::setBaud(uint32_t baud) { + if (!rs485_ok_ || baud == 0) + return false; + if (baud == baud_) + return true; + if (!waitTxComplete(frameTimeUs(SERIAL_TX_BUFFER_SIZE) + kTxSlackUs)) + return false; + rs485_ok_ = configureRS485(baud); // no clock/pin/NVIC teardown, unlike end()+begin() + return rs485_ok_; } -// Wait for the hardware TX complete flag, not just the software TX buffer depth. -// previously used availableForWrite(), but that only tells us the queue has room, not that the final byte -// has left the shift register and the bus is safe to deselect. +// Done = core ring buffer drained, no HAL transfer in flight, and the last stop bit +// has left the shift register. TC alone can read 1 for a few cycles at a ring-buffer +// wrap, before the TX-complete ISR queues the next chunk. bool RS485Bus::waitTxComplete(uint32_t timeout_us) { - USART_TypeDef *u = uart_.getHandle()->Instance; + UART_HandleTypeDef *h = getHandle(); const uint32_t start = micros(); - while ((u->ISR & USART_ISR_TC) == 0) { + while (_serial.tx_head != _serial.tx_tail || serial_tx_active(&_serial) || !__HAL_UART_GET_FLAG(h, UART_FLAG_TC)) { if (micros() - start >= timeout_us) return false; } - return true; } @@ -100,59 +121,65 @@ uint32_t RS485Bus::frameTimeUs(size_t len) const { return (uint32_t)((len * 10ULL * 1000000ULL) / baud_); } -// RS485Device methods +void RS485Bus::deselectAll() { + for (size_t i = 0; i < sel_count_; i++) { + digitalWrite(sels_[i], LOW); + } +} -bool RS485Device::beginTransaction() { - bus_.deselectAll(); - bool ok = true; - if (bus_.baud_ != baud_) - ok = bus_.setBaud(baud_); +void RS485Bus::select(size_t idx) { + deselectAll(); + if (idx < sel_count_) { + digitalWrite(sels_[idx], HIGH); + } +} - bus_.select(idx_); +// RS485Device methods - while (uart.available()) - uart.read(); +bool RS485Device::beginTransaction() { + bus.deselectAll(); + if (!bus.setBaud(baud_)) + return false; // never select a device at the wrong baud - return ok; + bus.select(idx_); + while (bus.available()) + bus.read(); + return true; } size_t RS485Device::read(uint8_t *dst, size_t len, uint32_t latency_us) { - // Don't start response clock while our own request is still sending - bool complete = bus_.waitTxComplete(bus_.frameTimeUs(SERIAL_TX_BUFFER_SIZE) + kTxSlackUs); - - if (!complete) { - return 0; // Error Code 0; Bus is still transmitting + // Don't start the response clock while our own request is still on the wire. + if (!bus.waitTxComplete(bus.frameTimeUs(SERIAL_TX_BUFFER_SIZE) + kTxSlackUs)) { + return 0; // bus still transmitting } - const uint32_t budget = latency_us + bus_.frameTimeUs(len) + kMarginUs; + const uint32_t budget = latency_us + bus.frameTimeUs(len) + kMarginUs; const uint32_t start = micros(); size_t n = 0; while (n < len) { - if (uart.available()) { - dst[n++] = (uint8_t)uart.read(); - continue; // ensure all bytes are read before checking timeout + if (bus.available()) { + dst[n++] = (uint8_t)bus.read(); + continue; // drain available bytes before checking the timeout } - if (micros() - start >= budget) { break; } } - return n; } void RS485Device::endTransaction() { - bus_.waitTxComplete(bus_.frameTimeUs(SERIAL_TX_BUFFER_SIZE) + kTxSlackUs); - bus_.deselectAll(); + bus.waitTxComplete(bus.frameTimeUs(SERIAL_TX_BUFFER_SIZE) + kTxSlackUs); + bus.deselectAll(); } -// Namespace Globals +// Namespace globals namespace RS485s { namespace { constexpr uint32_t kEncBaud = 2000000; // AMT24 2 Mbps data rate -constexpr uint32_t kTvcBaud = 2000000; // TODO - Check Baud rate for TVC +constexpr uint32_t kTvcBaud = 2000000; // TODO - check baud rate for TVC constexpr uint32_t kDrvBaud = 115200; // TODO - check driver's RS485 config; placeholder // Index in each array == device index below @@ -160,8 +187,8 @@ const uint32_t bus6_sels[] = {PIN_TVC_PITCH_SEL, PIN_ENC_OX_SEL, PIN_DRV_OX_SEL} const uint32_t bus2_sels[] = {PIN_TVC_YAW_SEL, PIN_ENC_FU_SEL, PIN_DRV_FU_SEL}; } // namespace -RS485Bus bus6(PIN_RS485_6_RX, PIN_RS485_6_TX, PIN_RS485_6_DE, bus6_sels, std::size(bus6_sels)); -RS485Bus bus2(PIN_RS485_2_RX, PIN_RS485_2_TX, PIN_RS485_2_DE, bus2_sels, std::size(bus2_sels)); +RS485Bus bus6(PIN_RS485_6_RX, PIN_RS485_6_TX, PIN_RS485_6_DE, bus6_sels); +RS485Bus bus2(PIN_RS485_2_RX, PIN_RS485_2_TX, PIN_RS485_2_DE, bus2_sels); RS485Device tvc_pitch(bus6, 0, kTvcBaud); RS485Device enc_ox(bus6, 1, kEncBaud); @@ -172,11 +199,9 @@ RS485Device enc_fu(bus2, 1, kEncBaud); RS485Device drv_fu(bus2, 2, kDrvBaud); bool begin() { - bool ok = true; - ok &= bus6.begin(kEncBaud); - ok &= bus2.begin(kEncBaud); - ok &= bus6.deModeActive(); - ok &= bus2.deModeActive(); + bus6.begin(kEncBaud); + bus2.begin(kEncBaud); + const bool ok = static_cast(bus6) && static_cast(bus2); if (!ok) CommsSerial.println("ERROR: RS485 init failed"); return ok; diff --git a/firmware/lib/RS485/RS485.h b/firmware/lib/RS485/RS485.h index 0814ef2..82b74e2 100644 --- a/firmware/lib/RS485/RS485.h +++ b/firmware/lib/RS485/RS485.h @@ -3,61 +3,87 @@ #include #include +#if defined(USE_HALV2_DRIVER) +#error "RS485Bus is written against the legacy STM32 HAL UART handle (UART_HandleTypeDef)" +#endif -// DE pin is passed to the Uart constructor as RTS so that the core muxes it, -// and RS485Bus converts RTS flow control into DE mode. -// NOTE: (re)initialize only via RS485Bus::begin()/setBaud(). Calling uart().begin() -// directly re-enables RTSE and drops DE mode. -class RS485Bus { +// RS485 bus on an STM32 U(S)ART with hardware driver-enable (DE). +// +// Init sequence (begin()): +// 1. DE held LOW as a GPIO (transceiver in receive), all selects LOW. +// 2. Uart::begin(): core enables clocks, muxes RX/TX, sets up NVIC, +// runs HAL_UART_Init and arms interrupt-driven RX. +// 3. HAL_RS485Ex_Init() on the core's own handle: DE mode, polarity, DEAT/DEDT. +// 4. RX re-armed through the core (step 3 resets RxState, orphaning the +// core's pending Receive_IT). +// 5. DE pin muxed to its USART AF with the core's pinmap_pinout(), only after +// the peripheral is already in DE mode, so the pin is never a live RTS output. +// +// begin()/end() are virtual in Uart, so every (re)init path, including a plain +// bus.begin(baud), goes through the RS485 configuration. +class RS485Bus : public Uart { public: - RS485Bus(uint32_t rx, uint32_t tx, uint32_t de, const uint32_t *sels, size_t sel_count); + // N is deduced from the select-pin array. DE is deliberately NOT passed to the + // core as RTS: that would enable RTS flow control and make the pin a live RTS + // output during init. See muxDE(). + template + RS485Bus(uint32_t rx, uint32_t tx, uint32_t de, const uint32_t (&sels)[N]) + : Uart(rx, tx), de_(de), sels_(sels), sel_count_(N) {} - bool begin(const uint32_t baud); // once, at startup - bool setBaud(uint32_t baud); // waits for TX to finish - uint32_t baud() const { return baud_; } - Uart &uart() { return uart_; } - const Uart &uart() const { return uart_; } + // Only pointer to sels is stored, so a temporary array would dangle: + // RS485Bus b(rx, tx, de, {PIN_A, PIN_B}) must not compile. + template RS485Bus(uint32_t rx, uint32_t tx, uint32_t de, const uint32_t (&&sels)[N]) = delete; + + using Uart::begin; // keep begin(baud) visible alongside the override below + void begin(unsigned long baud, uint16_t config) override; // config must be SERIAL_8N1 + void end() override; + operator bool() override; // core init OK and DE mode confirmed in hardware + + bool setBaud(uint32_t baud); // in-place reconfigure; waits for TX to drain first + uint32_t baud() const { + return baud_; + } - void deselectAll(); bool waitTxComplete(uint32_t timeout_us); uint32_t frameTimeUs(size_t len) const; // 8N1: 10 bits per byte - bool deModeActive(); - Uart uart_; - -private: - - const uint32_t *sels_; - size_t sel_count_; - uint32_t baud_ = 0; + void deselectAll(); +private: + friend class RS485Device; void select(size_t idx); - static void applyDE(USART_TypeDef *u); // requires UE = 0 + bool configureRS485(uint32_t baud); // HAL_RS485Ex_Init on the core handle + RX re-arm + bool muxDE(); // DE pin -> USART AF via the core's pinmap - friend class RS485Device; - bool initAt(uint32_t baud); // core begin() + RS485 delta + const uint32_t de_; + const uint32_t *const sels_; + const size_t sel_count_; + uint32_t baud_ = 0; + bool rs485_ok_ = false; }; class RS485Device { public: - RS485Device(RS485Bus &bus, size_t idx, uint32_t baud) - : uart(bus.uart_), bus_(bus), idx_(idx), baud_(baud) {} + RS485Device(RS485Bus &bus, size_t idx, uint32_t baud) : bus(bus), idx_(idx), baud_(baud) {} - // Switches bus to this device's baud if needed, selects it, flushes stale RX. - // Returns false if baud switch failed. + // Deselects all, switches the bus to this device's baud if needed, selects this + // device and flushes stale RX. Returns false (with nothing selected) on failure. bool beginTransaction(); void endTransaction(); // Returns number of bytes received (== len on success). size_t read(uint8_t *dst, size_t len, uint32_t latency_us = 500); - void setBaud(uint32_t baud) { baud_ = baud; } // applied at next beginTransaction() - uint32_t baud() const { return baud_; } + void setBaud(uint32_t baud) { + baud_ = baud; + } // applied at next beginTransaction() + uint32_t baud() const { + return baud_; + } - Uart &uart; + RS485Bus &bus; // is-a Uart: use bus.write() etc. inside a transaction private: - RS485Bus &bus_; - size_t idx_; + const size_t idx_; uint32_t baud_; }; From 69c16db359b732fea4d5347dd45b97c3e86c65ce Mon Sep 17 00:00:00 2001 From: Rishab Date: Thu, 1 Oct 2026 20:20:53 -0400 Subject: [PATCH 08/10] Renamed muxDE to ConnfigureDe and changed configureRS485() to direclty set hardware flow control using HAl instead of letting HAL init method configure it --- firmware/lib/RS485/RS485.cpp | 98 ++++++++++++++++++------------------ firmware/lib/RS485/RS485.h | 27 +++++++--- 2 files changed, 68 insertions(+), 57 deletions(-) diff --git a/firmware/lib/RS485/RS485.cpp b/firmware/lib/RS485/RS485.cpp index 8d2d8f9..cc1667f 100644 --- a/firmware/lib/RS485/RS485.cpp +++ b/firmware/lib/RS485/RS485.cpp @@ -6,19 +6,23 @@ #include "stm32yyxx_ll_usart.h" namespace { + // in Microseconds constexpr uint32_t kMarginUs = 200; // TODO: tune against response time constexpr uint32_t kTxSlackUs = 2000; -// DE timing, in sample-time units (1/16 bit at OVER16); HAL range 0..31. -// Assertion: DE rises this long before the start bit. Must exceed the transceiver's -// driver-enable time (t_ZH / t_ZL in its datasheet). 31 = 1.94 bit = 0.97 us at 2 Mbps. -constexpr uint32_t kDeAssertTime = 31; -// Deassertion: DE held this long after the end of the last stop bit. -constexpr uint32_t kDeDeassertTime = 1; +// DE timing in USART sample times (1/16 bit at OVER16), written straight into +// CR1.DEAT/DEDT. Register range 0..31. A sample time scales with 1/baud, so these +// give the shortest real time at the highest baud: size them for kEncBaud (2 Mbps, +// 1 sample = 31.25 ns); lower bauds only get more margin. +// Assert: 31 samples = 969 ns at 2 Mbps. Must exceed transceiver t_ZH / t_ZL. +// Deassert: 1 sample = 31 ns at 2 Mbps. +// Using sample time since STM32 splits each bit into samples +constexpr uint32_t kDeAssertTime = 31; // IN SAMPLES +constexpr uint32_t kDeDeassertTime = 1; // IN SAMPLES +static_assert(kDeAssertTime <= 31 && kDeDeassertTime <= 31, "DEAT/DEDT are 5-bit fields"); } // namespace void RS485Bus::begin(unsigned long baud, uint16_t config) { - rs485_ok_ = false; // Transceiver in receive and all devices deselected until the USART owns DE. pinMode(de_, OUTPUT); @@ -28,11 +32,7 @@ void RS485Bus::begin(unsigned long baud, uint16_t config) { digitalWrite(sels_[i], LOW); } - Uart::begin(baud, config); // clocks, RX/TX mux, NVIC, HAL_UART_Init, RX armed - if (!Uart::operator bool() || config != SERIAL_8N1) // frameTimeUs() assumes 8N1 - return; - - rs485_ok_ = configureRS485(baud) && muxDE(); + rs485_ok_ = setBaud(baud) && configureDE(); } void RS485Bus::end() { @@ -43,38 +43,27 @@ void RS485Bus::end() { digitalWrite(de_, LOW); } -RS485Bus::operator bool() { - // rs485_ok_ first: before begin() the handle's Instance is null. - return rs485_ok_ && Uart::operator bool() && LL_USART_IsEnabledDEMode(getHandle()->Instance); -} +// Configure the UART for half-duplex RS485: disable the peripheral while reprogramming +// the DE/RTS timing and polarity bits, +// then re-enable the peripheral -// Reconfigure the core's own handle for RS485, in place. -// HAL_RS485Ex_Init re-runs UART_SetConfig and UART_CheckIdleState; the latter sets -// RxState = READY, which orphans the core's pending 1-byte Receive_IT (the ISR would -// then discard every byte). So: stop RX explicitly, reconfigure, re-arm RX through -// the core's own entry point, and verify it is armed. -// -// NOTE: UART_CheckIdleState waits for TEACK with HAL_UART_TIMEOUT_VALUE (~9 h), not a -// short timeout. It only blocks if the USART kernel clock is dead; the core's boot-time -// HAL_UART_Init has the same exposure. -bool RS485Bus::configureRS485(uint32_t baud) { +void RS485Bus::configureRS485() { UART_HandleTypeDef *h = getHandle(); - HAL_NVIC_DisableIRQ(_serial.irq); // core does the same around Receive_IT (handle lock) - HAL_UART_AbortReceive(h); - h->Init.BaudRate = baud; - h->Init.HwFlowCtl = UART_HWCONTROL_NONE; // the pin is DE, not RTS - const bool ok = HAL_RS485Ex_Init(h, UART_DE_POLARITY_HIGH, kDeAssertTime, kDeDeassertTime) == HAL_OK; - HAL_NVIC_EnableIRQ(_serial.irq); - if (!ok) - return false; + __HAL_UART_DISABLE(h); // Clear UE so that DEM/DEP/DEAT/DEDT/BRR are writable + CLEAR_BIT(h->Instance->CR3, USART_CR3_RTSE | USART_CR3_CTSE); // Explicityly clear RTSE since RTS and DE share same AF. Allows + // for safer hardware flow control - uart_attach_rx_callback(&_serial, _rx_complete_irq); - if ((HAL_UART_GetState(h) & HAL_UART_STATE_BUSY_RX) != HAL_UART_STATE_BUSY_RX) - return false; // RX not re-armed: fail closed + SET_BIT(h->Instance->CR3, USART_CR3_DEM); // Enable Driver Enable mode in the CR3 register by setting DEM bit + MODIFY_REG(h->Instance->CR3, USART_CR3_DEP, UART_DE_POLARITY_HIGH); // Set driver polarity high - baud_ = baud; - return true; + // Set driver enable assertion and deassertion times + const uint32_t de_times = + (kDeAssertTime << UART_CR1_DEAT_ADDRESS_LSB_POS) | (kDeDeassertTime << UART_CR1_DEDT_ADDRESS_LSB_POS); + + MODIFY_REG(h->Instance->CR1, (USART_CR1_DEDT | USART_CR1_DEAT), de_times); + + __HAL_UART_ENABLE(h); } // Mux DE with the same call the core uses for RX/TX/RTS. RTS and DE share one AF on @@ -82,7 +71,7 @@ bool RS485Bus::configureRS485(uint32_t baud) { // pinmap_pinout() hangs in Error_Handler() on an unmapped pin, and because lookup is // first-match: if this pin's DE function is on an _ALTx entry, fail loudly here rather // than mux the wrong AF. -bool RS485Bus::muxDE() { +bool RS485Bus::configureDE() { const PinName pn = digitalPinToPinName(de_); if (pinmap_peripheral(pn, PinMap_UART_RTS) != (void *)getHandle()->Instance) return false; @@ -95,10 +84,10 @@ bool RS485Bus::setBaud(uint32_t baud) { return false; if (baud == baud_) return true; - if (!waitTxComplete(frameTimeUs(SERIAL_TX_BUFFER_SIZE) + kTxSlackUs)) - return false; - rs485_ok_ = configureRS485(baud); // no clock/pin/NVIC teardown, unlike end()+begin() - return rs485_ok_; + + Uart::begin(baud); + configureRS485(); + return Uart::operator bool(); // Checking underlying Uart ok } // Done = core ring buffer drained, no HAL transfer in flight, and the last stop bit @@ -138,8 +127,8 @@ void RS485Bus::select(size_t idx) { bool RS485Device::beginTransaction() { bus.deselectAll(); - if (!bus.setBaud(baud_)) - return false; // never select a device at the wrong baud + if (!bus.ready() || !bus.setBaud(baud_)) + return false; // never select a device on a dead bus or at the wrong baud bus.select(idx_); while (bus.available()) @@ -201,9 +190,18 @@ RS485Device drv_fu(bus2, 2, kDrvBaud); bool begin() { bus6.begin(kEncBaud); bus2.begin(kEncBaud); - const bool ok = static_cast(bus6) && static_cast(bus2); - if (!ok) - CommsSerial.println("ERROR: RS485 init failed"); - return ok; + if (!bus6.ready()){ + CommsSerial.println("ERROR: RS485 bus6 (USART6) init failed"); + } + + if (!bus2.ready()){ + CommsSerial.println("ERROR: RS485 bus2 (USART2) init failed"); + } + + if(!bus6){ + return false; + } + + return true; } } // namespace RS485s \ No newline at end of file diff --git a/firmware/lib/RS485/RS485.h b/firmware/lib/RS485/RS485.h index 82b74e2..a0cd98f 100644 --- a/firmware/lib/RS485/RS485.h +++ b/firmware/lib/RS485/RS485.h @@ -13,14 +13,16 @@ // 1. DE held LOW as a GPIO (transceiver in receive), all selects LOW. // 2. Uart::begin(): core enables clocks, muxes RX/TX, sets up NVIC, // runs HAL_UART_Init and arms interrupt-driven RX. -// 3. HAL_RS485Ex_Init() on the core's own handle: DE mode, polarity, DEAT/DEDT. -// 4. RX re-armed through the core (step 3 resets RxState, orphaning the -// core's pending Receive_IT). +// 3. configureRS485(): with UE cleared, set DEM/DEP/DEAT/DEDT and BRR directly. +// Clearing UE preserves configuration (incl. RXNEIE), so the core's pending +// Receive_IT survives; no re-arm needed. Waits for TEACK/REACK on re-enable. +// 4. DE pin muxed to its USART AF ... // 5. DE pin muxed to its USART AF with the core's pinmap_pinout(), only after // the peripheral is already in DE mode, so the pin is never a live RTS output. // // begin()/end() are virtual in Uart, so every (re)init path, including a plain // bus.begin(baud), goes through the RS485 configuration. + class RS485Bus : public Uart { public: // N is deduced from the select-pin array. DE is deliberately NOT passed to the @@ -37,7 +39,17 @@ class RS485Bus : public Uart { using Uart::begin; // keep begin(baud) visible alongside the override below void begin(unsigned long baud, uint16_t config) override; // config must be SERIAL_8N1 void end() override; - operator bool() override; // core init OK and DE mode confirmed in hardware + + // True only if begin() completed every step: core init, DE mode, baud, DE pin muxed. + // Cleared by end() and by a failed setBaud(). + bool ready() const { + return rs485_ok_; + } + + // Kept only so a Uart& caller can't get a misleading 'true'. Use ready() in RS485 code. + operator bool() override { + return ready(); + } bool setBaud(uint32_t baud); // in-place reconfigure; waits for TX to drain first uint32_t baud() const { @@ -51,8 +63,8 @@ class RS485Bus : public Uart { private: friend class RS485Device; void select(size_t idx); - bool configureRS485(uint32_t baud); // HAL_RS485Ex_Init on the core handle + RX re-arm - bool muxDE(); // DE pin -> USART AF via the core's pinmap + void configureRS485(); // HAL_RS485Ex_Init on the core handle + RX re-arm + bool configureDE(); // DE pin -> USART AF via the core's pinmap const uint32_t de_; const uint32_t *const sels_; @@ -99,4 +111,5 @@ extern RS485Device drv_ox; extern RS485Device tvc_yaw; extern RS485Device enc_fu; extern RS485Device drv_fu; -} // namespace RS485s \ No newline at end of file +} // namespace RS485s + From 3ba0099a14bde0ff0f4c58dd6b81025e2c75ae20 Mon Sep 17 00:00:00 2001 From: Rishab Date: Fri, 2 Oct 2026 02:09:32 -0400 Subject: [PATCH 09/10] removed unecessary headers, and fixed some minor issues with code structure --- firmware/lib/RS485/RS485.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/firmware/lib/RS485/RS485.cpp b/firmware/lib/RS485/RS485.cpp index cc1667f..ef9c2fe 100644 --- a/firmware/lib/RS485/RS485.cpp +++ b/firmware/lib/RS485/RS485.cpp @@ -3,7 +3,7 @@ #include "PeripheralPins.h" #include "ec_pins.h" #include "pinmap.h" -#include "stm32yyxx_ll_usart.h" + namespace { // in Microseconds @@ -17,8 +17,8 @@ constexpr uint32_t kTxSlackUs = 2000; // Assert: 31 samples = 969 ns at 2 Mbps. Must exceed transceiver t_ZH / t_ZL. // Deassert: 1 sample = 31 ns at 2 Mbps. // Using sample time since STM32 splits each bit into samples -constexpr uint32_t kDeAssertTime = 31; // IN SAMPLES -constexpr uint32_t kDeDeassertTime = 1; // IN SAMPLES +constexpr uint32_t kDeAssertTime = 5; // IN SAMPLES +constexpr uint32_t kDeDeassertTime = 5; // IN SAMPLES static_assert(kDeAssertTime <= 31 && kDeDeassertTime <= 31, "DEAT/DEDT are 5-bit fields"); } // namespace @@ -190,15 +190,14 @@ RS485Device drv_fu(bus2, 2, kDrvBaud); bool begin() { bus6.begin(kEncBaud); bus2.begin(kEncBaud); + if (!bus6.ready()){ CommsSerial.println("ERROR: RS485 bus6 (USART6) init failed"); + return false; } if (!bus2.ready()){ CommsSerial.println("ERROR: RS485 bus2 (USART2) init failed"); - } - - if(!bus6){ return false; } From 468de375780d26b7732c17c7feb6cad42facc976 Mon Sep 17 00:00:00 2001 From: Rishab Date: Sat, 3 Oct 2026 16:55:02 -0400 Subject: [PATCH 10/10] Fixed the setBaud method so virtual dispatch doesn't just loop back to our begin() method. --- firmware/lib/RS485/RS485.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/firmware/lib/RS485/RS485.cpp b/firmware/lib/RS485/RS485.cpp index ef9c2fe..9dc03af 100644 --- a/firmware/lib/RS485/RS485.cpp +++ b/firmware/lib/RS485/RS485.cpp @@ -80,14 +80,15 @@ bool RS485Bus::configureDE() { } bool RS485Bus::setBaud(uint32_t baud) { - if (!rs485_ok_ || baud == 0) + if (baud == 0) return false; if (baud == baud_) return true; - Uart::begin(baud); + Uart::begin(baud, SERIAL_8N1); configureRS485(); - return Uart::operator bool(); // Checking underlying Uart ok + baud_ = baud; + return Uart::operator bool(); // Checking underlying Uart ok } // Done = core ring buffer drained, no HAL transfer in flight, and the last stop bit