Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 207 additions & 0 deletions firmware/lib/RS485/RS485.cpp
Original file line number Diff line number Diff line change
@@ -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) {
Comment thread
RobertJN64 marked this conversation as resolved.
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() {
Comment thread
RobertJN64 marked this conversation as resolved.
bus6.begin(kEncBaud);
bus2.begin(kEncBaud);

if (!bus6.ready()){
CommsSerial.println("ERROR: RS485 bus6 (USART6) init failed");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return false in these

@Rishabg24 Rishabg24 Oct 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. My only concern is that here if we return false immediately, then in the case that both bus6 and bus2 fail, we don't know the latter fails until we fix bus6. Whereas if we have a flag, and let both conditionals evaluate then return false, we will know if both busses or just one of them is broken at the same time.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah - I would be fine with a flag as well - issue was that previously we weren't failing on bus2 status at all.

return false;
}

if (!bus2.ready()){
CommsSerial.println("ERROR: RS485 bus2 (USART2) init failed");
return false;
}

return true;
}
} // namespace RS485s
115 changes: 115 additions & 0 deletions firmware/lib/RS485/RS485.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#pragma once
#include <Arduino.h>
#include <cstddef>
#include <cstdint>

#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 <size_t N>
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 <size_t N> 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

21 changes: 10 additions & 11 deletions firmware/lib/hardware_mapping/ec_pins.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <Arduino.h>

#include "RS485.h"
#include "SPI.h"

// UARTS
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading
Loading