diff --git a/firmware/lib/RS485/RS485.cpp b/firmware/lib/RS485/RS485.cpp new file mode 100644 index 0000000..9dc03af --- /dev/null +++ b/firmware/lib/RS485/RS485.cpp @@ -0,0 +1,207 @@ +#include "RS485.h" +#include "CommsSerial.h" +#include "PeripheralPins.h" +#include "ec_pins.h" +#include "pinmap.h" + + +namespace { + // in Microseconds +constexpr uint32_t kMarginUs = 200; // TODO: tune against response time +constexpr uint32_t kTxSlackUs = 2000; + +// 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 = 5; // IN SAMPLES +constexpr uint32_t kDeDeassertTime = 5; // IN SAMPLES +static_assert(kDeAssertTime <= 31 && kDeDeassertTime <= 31, "DEAT/DEDT are 5-bit fields"); +} // namespace + +void RS485Bus::begin(unsigned long baud, uint16_t config) { + + // 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); + } + + rs485_ok_ = setBaud(baud) && configureDE(); +} + +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); +} + +// Configure the UART for half-duplex RS485: disable the peripheral while reprogramming +// the DE/RTS timing and polarity bits, +// then re-enable the peripheral + +void RS485Bus::configureRS485() { + UART_HandleTypeDef *h = getHandle(); + + __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 + + 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 + + // 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 +// 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::configureDE() { + 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; +} + +bool RS485Bus::setBaud(uint32_t baud) { + if (baud == 0) + return false; + if (baud == baud_) + return true; + + Uart::begin(baud, SERIAL_8N1); + configureRS485(); + 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 +// 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) { + UART_HandleTypeDef *h = getHandle(); + const uint32_t start = micros(); + + 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; +} + +uint32_t RS485Bus::frameTimeUs(size_t len) const { + if (baud_ == 0) + return 0; + return (uint32_t)((len * 10ULL * 1000000ULL) / baud_); +} + +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); + } +} + +// RS485Device methods + +bool RS485Device::beginTransaction() { + bus.deselectAll(); + 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()) + bus.read(); + return true; +} + +size_t RS485Device::read(uint8_t *dst, size_t len, uint32_t latency_us) { + // 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 start = micros(); + size_t n = 0; + + while (n < len) { + 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(); +} + +// 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 + +// Index in each array == device index below +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); +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); +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() { + 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"); + 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 new file mode 100644 index 0000000..a0cd98f --- /dev/null +++ b/firmware/lib/RS485/RS485.h @@ -0,0 +1,115 @@ +#pragma once +#include +#include +#include + +#if defined(USE_HALV2_DRIVER) +#error "RS485Bus is written against the legacy STM32 HAL UART handle (UART_HandleTypeDef)" +#endif + +// 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. 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 + // 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) {} + + // 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; + + // 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 { + return baud_; + } + + bool waitTxComplete(uint32_t timeout_us); + uint32_t frameTimeUs(size_t len) const; // 8N1: 10 bits per byte + void deselectAll(); + +private: + friend class RS485Device; + void select(size_t idx); + 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_; + 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) : bus(bus), idx_(idx), baud_(baud) {} + + // 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_; + } + + RS485Bus &bus; // is-a Uart: use bus.write() etc. inside a transaction + +private: + const 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 drv_ox; +extern RS485Device tvc_yaw; +extern RS485Device enc_fu; +extern RS485Device drv_fu; +} // namespace RS485s + diff --git a/firmware/lib/hardware_mapping/ec_pins.h b/firmware/lib/hardware_mapping/ec_pins.h index 9eb8cc9..aa7cc50 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 #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 #define PIN_ENC_FU_SEL PD10 // SPI diff --git a/firmware/src/ec_main.cpp b/firmware/src/ec_main.cpp index c45a242..7ef735e 100644 --- a/firmware/src/ec_main.cpp +++ b/firmware/src/ec_main.cpp @@ -11,14 +11,12 @@ #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 -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); @@ -35,10 +33,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 +49,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();