Repository navigation
Features/rs485 #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c7ce4f5
d9e1821
3322e79
c43c2b0
a418251
59f3d20
46e8035
69c16db
3ba0099
468de37
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) { | ||
| 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() { | ||
|
RobertJN64 marked this conversation as resolved.
|
||
| bus6.begin(kEncBaud); | ||
| bus2.begin(kEncBaud); | ||
|
|
||
| if (!bus6.ready()){ | ||
| CommsSerial.println("ERROR: RS485 bus6 (USART6) init failed"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. return false in these
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| 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 | ||
|
|
Uh oh!
There was an error while loading. Please reload this page.