Skip to content

Instantly share code, notes, and snippets.

@sjlongland
Created September 18, 2025 02:23
Show Gist options
  • Select an option

  • Save sjlongland/4421dbf313f6abf814ce60284e4759ba to your computer and use it in GitHub Desktop.

Select an option

Save sjlongland/4421dbf313f6abf814ce60284e4759ba to your computer and use it in GitHub Desktop.
RS-485 on Zephyr
&uart0 {
compatible = "nordic,nrf-uarte";
status = "okay";
current-speed = <115200>;
pinctrl-0 = <&uart0_default>;
pinctrl-1 = <&uart0_sleep>;
pinctrl-names = "default", "sleep";
/*
* Zephyr at this time does not have a RS-485 flow control primitive in
* device tree.
*
* https://github.com/zephyrproject-rtos/zephyr/issues/32733
* https://github.com/zephyrproject-rtos/zephyr/pull/80305
*
* The latter seems to specifically focus on STM32, which is not what
* we're using here. So we'll have to wing it for now.
*/
rs485_p1: rs485_p1 {
compatible = "widesky,rs485";
status = "okay";
current-speed = <115200>;
rs485-timer = <&timer3>;
rs485-de-gpios = <&gpio1 1 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>;
rs485-re-gpios = <&gpio0 14 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
rs485-separate-re;
rs485-assertion-time-de-us = <25>;
rs485-deassertion-time-de-us = <25>;
rs485-assertion-time-re-us = <25>;
rs485-deassertion-time-re-us = <25>;
port = <0>;
};
};
&timer3 {
status = "okay";
/*
* Frequency out = 16MHz / 2⁵ = 16MHz / 32 = 500kHz
*/
prescaler = <5>;
};
/*
* Half-working and quite buggy RS-485 "overlay" UART driver.
* © 2025 WideSky.Cloud Pty Ltd
*
* Based on emulated Zephyr UART
* Copyright © 2023 Fabian Blatz
* Copyright © 2024 grandcentrix GmbH
*
* Some ideas also taken from
* https://github.com/Riphiphip/zephyr-uart-driver-example/
*/
#define DT_DRV_COMPAT widesky_rs485
#include <assert.h>
#include <errno.h>
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/counter.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/ring_buffer.h>
#include <zephyr/sys/util.h>
#include <app/drivers/rs485.h>
LOG_MODULE_REGISTER(rs485, CONFIG_UART_LOG_LEVEL);
#define WSH_RS485_DIR_m (3)
#define WSH_RS485_DIR_SELECTED_p (2)
#define WSH_RS485_DIR_SELECTED_m (WSH_RS485_DIR_m << WSH_RS485_DIR_SELECTED_p)
#define WSH_RS485_DIR_SELECTED_NONE \
(WSH_RS485_DIR_NONE << WSH_RS485_DIR_SELECTED_p)
#define WSH_RS485_DIR_SELECTED_RX \
(WSH_RS485_DIR_RXO << WSH_RS485_DIR_SELECTED_p)
#define WSH_RS485_DIR_SELECTED_TX \
(WSH_RS485_DIR_TXO << WSH_RS485_DIR_SELECTED_p)
#define WSH_RS485_RX_READY (1 << 4)
#define WSH_RS485_TX_READY (1 << 5)
#define WSH_RS485_TX_DONE (1 << 6)
#define WSH_RS485_TX_RQ (1 << 7)
#define WSH_RS485_RX_INT_EN (1 << 8)
#define WSH_RS485_TX_INT_EN (1 << 9)
#define WSH_RS485_RX_ERR (1 << 10)
#define WSH_RS485_TX_ERR (1 << 11)
#define WSH_RS485_LISTEN_BEFORE_TX (1 << 12)
#define WSH_RS485_TRANSITION_START (1 << 14)
#define WSH_RS485_TRANSITION_DONE (1 << 15)
struct rs485_config {
/*! UART device being "overlayed" */
const struct device* uart_dev;
/*! General purpose timer address being utilised */
const struct device* timer_dev;
/*! GPIO pin for Driver Enable */
struct gpio_dt_spec de;
/*! GPIO pin for Receiver Enable */
struct gpio_dt_spec re;
/*! RS-485 DE assert µs */
uint32_t de_assert;
/*! RS-485 DE dis-assert µs */
uint32_t de_deassert;
/*! RS-485 RE assert µs */
uint32_t re_assert;
/*! RS-485 RE dis-assert µs */
uint32_t re_deassert;
/*! RS-485 port number */
uint8_t port;
};
/* Device run time data */
struct rs485_data {
int64_t last_rx;
rs485_act_cb* act_cb;
void* act_cb_ctx;
#ifdef CONFIG_UART_INTERRUPT_DRIVEN
uart_irq_callback_user_data_t irq_cb;
void* irq_cb_ctx;
#endif
#ifdef CONFIG_UART_ASYNC_API
uart_callback_t async_cb;
void* async_cb_ctx;
#endif
#ifndef CONFIG_UART_USE_RUNTIME_CONFIGURE
uint32_t baud_rate;
#endif
struct k_event timer_evt;
uint32_t halfchar;
uint32_t state;
/*! Clear-to-send delay (half-chars) */
uint8_t cts_delay;
};
static int rs485_compute_delays(const struct device* dev,
const struct uart_config* uart_cfg) {
struct uart_config _uart_cfg;
if (uart_cfg == NULL) {
/*
* We weren't given a structure with configured data, so fetch
* it
*/
#ifdef CONFIG_UART_USE_RUNTIME_CONFIGURE
const struct rs485_config* cfg = dev->config;
int res = uart_config_get(cfg->uart_dev, &_uart_cfg);
if (res < 0) {
return res;
}
#else
/* Runtime configuration is disabled, so fudge it */
const struct rs485_data* data = dev->data;
_uart_cfg.baudrate = data->baud_rate;
_uart_cfg.parity = UART_CFG_PARITY_NONE;
_uart_cfg.stop_bits = UART_CFG_STOP_BITS_1;
_uart_cfg.data_bits = UART_CFG_DATA_BITS_8;
#endif
uart_cfg = &_uart_cfg;
}
/* Count frame size in bits: count start and (one of the) stop bits */
uint8_t frame_sz = 2;
switch (uart_cfg->parity) {
case UART_CFG_PARITY_ODD:
case UART_CFG_PARITY_EVEN:
case UART_CFG_PARITY_MARK:
case UART_CFG_PARITY_SPACE:
/* Count parity bit */
frame_sz++;
break;
case UART_CFG_PARITY_NONE:
default:
break;
}
switch (uart_cfg->stop_bits) {
case UART_CFG_STOP_BITS_2:
/* Count extra stop bit */
frame_sz++;
break;
case UART_CFG_STOP_BITS_1:
default:
break;
}
switch (uart_cfg->data_bits) {
case UART_CFG_DATA_BITS_5:
frame_sz += 5;
break;
case UART_CFG_DATA_BITS_6:
frame_sz += 6;
break;
case UART_CFG_DATA_BITS_7:
frame_sz += 7;
break;
case UART_CFG_DATA_BITS_8:
default:
frame_sz += 8;
break;
}
/* Compute the half-character period in µs for the given baud rate. */
return (1000000ul * frame_sz) / (2 * uart_cfg->baudrate);
}
static int rs485_get_en_direction_intl(const struct rs485_data* data) {
return (data->state & WSH_RS485_DIR_m);
}
static int rs485_get_act_direction_intl(const struct rs485_data* data) {
return (data->state & WSH_RS485_DIR_SELECTED_m)
>> WSH_RS485_DIR_SELECTED_p;
}
static int rs485_get_eff_direction(const struct rs485_data* data) {
return rs485_get_en_direction_intl(data)
& rs485_get_act_direction_intl(data);
}
static bool rs485_can_tx(const struct rs485_data* data) {
return rs485_get_en_direction_intl(data) & WSH_RS485_DIR_TXO;
}
static bool rs485_can_rx(const struct rs485_data* data) {
return rs485_get_en_direction_intl(data) & WSH_RS485_DIR_RXO;
}
static bool rs485_is_rx(const struct rs485_data* data) {
return rs485_get_eff_direction(data) & WSH_RS485_DIR_RXO;
}
#ifdef CONFIG_UART_INTERRUPT_DRIVEN
static bool rs485_is_tx(const struct rs485_data* data) {
return rs485_get_eff_direction(data) & WSH_RS485_DIR_TXO;
}
#endif
static void rs485_timer_isr(const struct device* timer_dev, uint8_t chan_id,
uint32_t ticks, void* user_data) {
const struct device* rs485_dev = user_data;
struct rs485_data* data = rs485_dev->data;
(void)chan_id;
(void)ticks;
if (data->state & WSH_RS485_TRANSITION_START) {
data->state
|= WSH_RS485_TRANSITION_DONE | WSH_RS485_DIR_SELECTED_TX;
data->state &= ~WSH_RS485_TRANSITION_START;
k_event_post(&(data->timer_evt), WSH_RS485_TRANSITION_DONE);
}
}
/*! Reset the RS-485 control lines to safe values! */
static void rs485_reset_ctl(const struct device* dev) {
const struct rs485_config* cfg = dev->config;
struct rs485_data* data = dev->data;
gpio_pin_set_dt(&cfg->re, 0);
gpio_pin_set_dt(&cfg->de, 0);
data->state
&= ~(WSH_RS485_DIR_SELECTED_RX | WSH_RS485_DIR_SELECTED_TX
| WSH_RS485_TRANSITION_START | WSH_RS485_TRANSITION_DONE);
}
static int rs485_setup_timer(const struct device* dev, uint32_t delay_us) {
const struct rs485_config* cfg = dev->config;
struct counter_alarm_cfg timer_cfg;
timer_cfg.callback = rs485_timer_isr;
timer_cfg.ticks = counter_us_to_ticks(cfg->timer_dev, delay_us);
timer_cfg.user_data = (void*)dev;
timer_cfg.flags = COUNTER_ALARM_CFG_EXPIRE_WHEN_LATE;
int res = counter_cancel_channel_alarm(cfg->timer_dev, 0);
if (res < 0) {
LOG_ERR("Could not cancel alarm (%d)", res);
return res;
}
res = counter_set_channel_alarm(cfg->timer_dev, 0, &timer_cfg);
if (res < 0) {
LOG_ERR("Could not configure alarm (%d)", res);
}
return res;
}
static int rs485_wait_us(const struct device* dev, uint32_t delay_us) {
const struct rs485_config* cfg = dev->config;
struct rs485_data* data = dev->data;
int res = rs485_setup_timer(dev, delay_us);
if (res < 0) {
return res;
}
res = counter_start(cfg->timer_dev);
if (res < 0) {
LOG_ERR("Could not start timer (%d)", res);
return res;
}
if (k_can_yield()) {
k_event_wait_all(&(data->timer_evt),
WSH_RS485_TRANSITION_DONE, true,
K_MSEC(100));
}
res = counter_stop(cfg->timer_dev);
if (res < 0) {
LOG_ERR("Could not stop timer (%d)", res);
return res;
}
return res;
}
static int64_t rs485_uptime_delta_us(int64_t uptime_ms) {
int64_t delta_ms = k_uptime_get() - uptime_ms;
if ((delta_ms >= (INT64_MAX / 1000))
|| (delta_ms <= (INT64_MIN / 1000))) {
/*
* Calculation will overflow, clamp it. We likely
* won't hit this, but better to be safe than sorry!
*/
return INT64_MAX;
} else {
/* Convert ms to us */
return delta_ms * 1000;
}
}
static int rs485_enter_tx(const struct device* dev) {
const struct rs485_config* cfg = dev->config;
struct rs485_data* data = dev->data;
/* Wait for clear-to-send */
data->state |= WSH_RS485_LISTEN_BEFORE_TX;
const int32_t cts_delay = data->halfchar * data->cts_delay;
int64_t last_rx = rs485_uptime_delta_us(data->last_rx);
while (last_rx < cts_delay) {
/* Wait a bit */
if (k_can_yield()) {
k_sleep(K_TICKS(1));
}
last_rx = rs485_uptime_delta_us(data->last_rx);
}
int res = gpio_pin_set_dt(&cfg->de, 1);
if (res < 0) {
return res;
}
res = gpio_pin_set_dt(&cfg->re, 0);
if (res < 0) {
return res;
}
data->state |= WSH_RS485_TRANSITION_START;
data->state &= ~WSH_RS485_TRANSITION_DONE;
uint32_t delay = cfg->de_assert;
if (cfg->re_deassert > delay) {
delay = cfg->re_deassert;
}
res = rs485_wait_us(dev, delay);
data->state &= ~WSH_RS485_LISTEN_BEFORE_TX;
if (res < 0) {
return res;
}
return res;
}
static int rs485_leave_tx(const struct device* dev) {
const struct rs485_config* cfg = dev->config;
struct rs485_data* data = dev->data;
data->state |= WSH_RS485_TRANSITION_START;
data->state &= ~WSH_RS485_TRANSITION_DONE;
int res = rs485_wait_us(dev, cfg->de_deassert + data->halfchar);
if (res < 0) {
return res;
}
res = gpio_pin_set_dt(&cfg->de, 0);
if (res < 0) {
return res;
}
return res;
}
static int rs485_enter_off(const struct device* dev) {
const struct rs485_config* cfg = dev->config;
struct rs485_data* data = dev->data;
int res = rs485_leave_tx(dev);
if (res < 0) {
return res;
}
res = gpio_pin_set_dt(&cfg->re, 0);
if (res < 0) {
return res;
}
data->state |= WSH_RS485_TRANSITION_START;
data->state &= ~WSH_RS485_TRANSITION_DONE;
uint32_t delay = cfg->de_deassert;
if (cfg->re_deassert > delay) {
delay = cfg->re_deassert;
}
res = rs485_wait_us(dev, delay);
if (res < 0) {
return res;
}
return res;
}
static int rs485_enter_rx(const struct device* dev) {
const struct rs485_config* cfg = dev->config;
struct rs485_data* data = dev->data;
int res = rs485_leave_tx(dev);
if (res < 0) {
return res;
}
res = gpio_pin_set_dt(&cfg->re, 1);
if (res < 0) {
return res;
}
data->state |= WSH_RS485_TRANSITION_START;
data->state &= ~WSH_RS485_TRANSITION_DONE;
uint32_t delay = cfg->re_assert;
if (cfg->de_deassert > delay) {
delay = cfg->de_deassert;
}
res = rs485_wait_us(dev, delay);
if (res < 0) {
return res;
}
return res;
}
static int rs485_set_tx(const struct device* dev, bool tx) {
struct rs485_data* data = dev->data;
int res;
if (tx) {
if (data->state & WSH_RS485_DIR_SELECTED_TX) {
/* Nothing to do */
return 0;
}
if (rs485_can_tx(data)) {
res = rs485_enter_tx(dev);
if (res < 0) {
rs485_reset_ctl(dev);
return res;
}
} else {
/* TX path disabled */
res = rs485_enter_off(dev);
if (res < 0) {
rs485_reset_ctl(dev);
return res;
}
}
} else {
if (data->state & WSH_RS485_DIR_SELECTED_RX) {
/* Nothing to do */
return 0;
}
if (rs485_can_rx(data)) {
res = rs485_enter_rx(dev);
if (res < 0) {
rs485_reset_ctl(dev);
return res;
}
} else {
/* RX path disabled */
res = rs485_enter_off(dev);
if (res < 0) {
rs485_reset_ctl(dev);
return res;
}
}
}
if (res == 0) {
if (tx) {
data->state &= ~WSH_RS485_DIR_SELECTED_RX;
data->state
|= (WSH_RS485_TX_RQ | WSH_RS485_DIR_SELECTED_TX);
} else {
data->state |= WSH_RS485_DIR_SELECTED_RX;
data->state
&= ~(WSH_RS485_TX_RQ | WSH_RS485_DIR_SELECTED_TX);
}
}
return res;
}
/*
* Set clear-to-send delay
*/
int rs485_cts_delay(const struct device* dev, uint8_t delay) {
struct rs485_data* data = dev->data;
data->cts_delay = delay;
return 0;
}
/*
* Change the permitted directions on the port.
*/
int rs485_set_en_direction(const struct device* dev, uint8_t dir) {
struct rs485_data* data = dev->data;
if (dir & ~WSH_RS485_DIR_m) {
/* Invalid bits set */
return -EINVAL;
}
data->state &= ~WSH_RS485_DIR_m;
data->state |= dir;
return rs485_set_tx(dev, data->state & WSH_RS485_TX_RQ);
}
/*
* Get the currently permitted direction.
*/
int rs485_get_en_direction(const struct device* dev) {
return rs485_get_en_direction_intl(dev->data);
}
/*
* Get the currently active direction.
*/
int rs485_get_act_direction(const struct device* dev) {
return rs485_get_act_direction_intl(dev->data);
}
/*
* Configure a callback for blinking LEDs on activity
*/
int rs485_set_activity_callback(const struct device* dev, rs485_act_cb* cb,
void* ctx) {
struct rs485_data* data = dev->data;
data->act_cb = cb;
data->act_cb_ctx = ctx;
return 0;
}
/*! Trigger the activity callback */
static void rs485_report_activity(const struct device* dev, uint8_t dir) {
struct rs485_data* data = dev->data;
if (dir == WSH_RS485_DIR_RXO) {
/* Reset the last receive timer */
data->last_rx = k_uptime_get();
}
if (data->act_cb) {
data->act_cb(dev, dir, data->act_cb_ctx);
}
}
static int rs485_poll_in(const struct device* dev, unsigned char* p_char) {
const struct rs485_config* cfg = dev->config;
if (rs485_is_rx(dev->data)) {
int res = uart_poll_in(cfg->uart_dev, p_char);
if (res >= 0) {
rs485_report_activity(dev, WSH_RS485_DIR_RXO);
}
return res;
} else {
/* Pretend nothing to receive */
return -1;
}
}
static void rs485_poll_out(const struct device* dev, unsigned char out_char) {
const struct rs485_config* cfg = dev->config;
if (rs485_can_tx(dev->data)) {
rs485_set_tx(dev, true);
rs485_report_activity(dev, WSH_RS485_DIR_TXO);
uart_poll_out(cfg->uart_dev, out_char);
rs485_set_tx(dev, false);
}
}
static int rs485_err_check(const struct device* dev) {
const struct rs485_config* cfg = dev->config;
return uart_err_check(cfg->uart_dev);
}
#ifdef CONFIG_UART_USE_RUNTIME_CONFIGURE
static int rs485_configure(const struct device* dev,
const struct uart_config* cfg) {
const struct rs485_config* dev_cfg = dev->config;
int res = uart_configure(dev_cfg->uart_dev, cfg);
if (res == 0) {
struct rs485_data* data = dev->data;
res = rs485_compute_delays(dev, cfg);
if (res >= 0) {
data->halfchar = res;
}
}
return res;
}
static int rs485_config_get(const struct device* dev,
struct uart_config* cfg) {
const struct rs485_config* dev_cfg = dev->config;
return uart_config_get(dev_cfg->uart_dev, cfg);
}
#endif /* CONFIG_UART_USE_RUNTIME_CONFIGURE */
#ifdef CONFIG_UART_INTERRUPT_DRIVEN
static void rs485_irq_cb(const struct device* uart_dev, void* ctx) {
/*
* NB: this is the underlying UART device! `dev` points to *that*,
* not a RS-485 dev!
*/
const struct device* rs485_dev = ctx;
struct rs485_data* data = rs485_dev->data;
int ret;
while (uart_irq_update(uart_dev) > 0) {
if (data->state & WSH_RS485_RX_INT_EN) {
ret = uart_irq_rx_ready(uart_dev);
if (ret < 0) {
data->state |= WSH_RS485_RX_ERR;
break;
} else if (ret > 0) {
rs485_report_activity(rs485_dev,
WSH_RS485_DIR_RXO);
data->state |= WSH_RS485_RX_READY;
} else {
data->state &= ~WSH_RS485_RX_READY;
}
}
if (data->state & WSH_RS485_TX_INT_EN) {
ret = uart_irq_tx_ready(uart_dev);
if (ret < 0) {
data->state |= WSH_RS485_TX_ERR;
break;
} else if (ret > 0) {
data->state |= WSH_RS485_TX_READY;
} else {
data->state &= ~WSH_RS485_TX_READY;
}
ret = uart_irq_tx_complete(uart_dev);
if (ret < 0) {
data->state |= WSH_RS485_TX_ERR;
break;
} else if (ret > 0) {
data->state |= WSH_RS485_TX_DONE;
if (rs485_can_tx(data)) {
/* Done transmitting, so we can turn
* off TX */
ret = rs485_set_tx(rs485_dev, false);
if (ret < 0) {
data->state
|= WSH_RS485_TX_ERR;
break;
}
}
} else {
rs485_report_activity(rs485_dev,
WSH_RS485_DIR_TXO);
data->state &= ~WSH_RS485_TX_DONE;
}
}
if (data->state
& (WSH_RS485_RX_READY | WSH_RS485_TX_READY
| WSH_RS485_TX_DONE)) {
/* Something is pending, so notify the callback */
if (data->irq_cb) {
data->irq_cb(rs485_dev, data->irq_cb_ctx);
/* Clear states after interrupt call */
data->state &= ~(WSH_RS485_RX_READY
| WSH_RS485_TX_READY
| WSH_RS485_TX_DONE);
}
} else {
/* Nothing to do */
break;
}
}
}
static int rs485_fifo_fill(const struct device* dev, const uint8_t* tx_data,
int size) {
const struct rs485_config* dev_cfg = dev->config;
if (rs485_can_tx(dev->data)) {
/* If we're not already transmitting, enter TX state */
if (!rs485_is_tx(dev->data)) {
int res = rs485_set_tx(dev, true);
if (res < 0) {
return res;
}
}
rs485_report_activity(dev, WSH_RS485_DIR_TXO);
return uart_fifo_fill(dev_cfg->uart_dev, tx_data, size);
} else {
/* TODO: can we mock a transmit here? */
return 0;
}
}
static int rs485_fifo_read(const struct device* dev, uint8_t* rx_data,
int size) {
if (rs485_is_rx(dev->data)) {
const struct rs485_config* dev_cfg = dev->config;
return uart_fifo_read(dev_cfg->uart_dev, rx_data, size);
} else {
return 0;
}
}
static int rs485_irq_tx_ready(const struct device* dev) {
const struct rs485_data* data = dev->data;
return (data->state & WSH_RS485_TX_READY) ? 1 : 0;
}
static int rs485_irq_rx_ready(const struct device* dev) {
const struct rs485_data* data = dev->data;
return (data->state & WSH_RS485_RX_READY) ? 1 : 0;
}
static int rs485_irq_is_pending(const struct device* dev) {
return rs485_irq_tx_ready(dev) || rs485_irq_rx_ready(dev);
}
static void rs485_irq_tx_enable(const struct device* dev) {
const struct rs485_config* dev_cfg = dev->config;
struct rs485_data* data = dev->data;
data->state |= WSH_RS485_TX_INT_EN;
return uart_irq_tx_enable(dev_cfg->uart_dev);
}
static void rs485_irq_rx_enable(const struct device* dev) {
const struct rs485_config* dev_cfg = dev->config;
struct rs485_data* data = dev->data;
data->state |= WSH_RS485_RX_INT_EN;
return uart_irq_rx_enable(dev_cfg->uart_dev);
}
static void rs485_irq_tx_disable(const struct device* dev) {
const struct rs485_config* dev_cfg = dev->config;
struct rs485_data* data = dev->data;
data->state &= ~WSH_RS485_TX_INT_EN;
return uart_irq_tx_disable(dev_cfg->uart_dev);
}
static void rs485_irq_rx_disable(const struct device* dev) {
const struct rs485_config* dev_cfg = dev->config;
struct rs485_data* data = dev->data;
data->state &= ~WSH_RS485_RX_INT_EN;
return uart_irq_rx_disable(dev_cfg->uart_dev);
}
static int rs485_irq_tx_complete(const struct device* dev) {
const struct rs485_data* data = dev->data;
return (data->state & WSH_RS485_TX_DONE) ? 1 : 0;
}
static void rs485_irq_callback_set(const struct device* dev,
uart_irq_callback_user_data_t cb,
void* user_data) {
const struct rs485_config* cfg = dev->config;
struct rs485_data* data = dev->data;
data->irq_cb = cb;
data->irq_cb_ctx = user_data;
/* Pass through `dev` as `ctx`, I promise to not modify it! */
uart_irq_callback_user_data_set(cfg->uart_dev, rs485_irq_cb,
(void*)dev);
}
static int rs485_irq_update(const struct device* dev) {
/*
* This is a no-op effectively, done by the ISR we install ourselves.
*/
(void)dev;
return 1;
}
#endif /* CONFIG_UART_INTERRUPT_DRIVEN */
#ifdef CONFIG_UART_ASYNC_API
static void rs485_async_cb(const struct device* uart_dev,
struct uart_event* evt, void* ctx) {
/*
* NB: this is the underlying UART device! `dev` points to *that*,
* not a RS-485 dev!
*/
const struct device* rs485_dev = ctx;
struct rs485_data* data = rs485_dev->data;
int ret;
switch (evt->type) {
case UART_TX_DONE:
case UART_TX_ABORTED:
if (rs485_can_tx(data)) {
/*
* Done transmitting, so we can turn
* off TX
*/
ret = rs485_set_tx(rs485_dev, false);
if (ret < 0) {
data->state |= WSH_RS485_TX_ERR;
return;
}
/* All good, notify callback */
if (data->async_cb) {
data->async_cb(rs485_dev, evt,
data->async_cb_ctx);
}
}
break;
case UART_RX_RDY:
case UART_RX_BUF_REQUEST:
rs485_report_activity(rs485_dev, WSH_RS485_DIR_RXO);
/* Fall thru */
default:
/* If in receive mode, pass through all other events */
if (rs485_is_rx(data) && data->async_cb) {
data->async_cb(rs485_dev, evt, data->async_cb_ctx);
}
break;
}
}
static int rs485_callback_set(const struct device* dev,
uart_callback_t callback, void* user_data) {
const struct rs485_config* cfg = dev->config;
struct rs485_data* data = dev->data;
data->async_cb = callback;
data->async_cb_ctx = user_data;
/* Pass through `dev` as `ctx`, I promise to not modify it! */
return uart_callback_set(cfg->uart_dev, rs485_async_cb, (void*)dev);
}
static int rs485_tx(const struct device* dev, const uint8_t* buf, size_t len,
int32_t timeout) {
const struct rs485_config* cfg = dev->config;
const struct rs485_data* data = dev->data;
if (rs485_can_tx(data)) {
/* If we're not already transmitting, enter TX state */
if (!rs485_is_tx(data)) {
int res = rs485_set_tx(dev, true);
if (res < 0) {
return res;
}
}
rs485_report_activity(dev, WSH_RS485_DIR_TXO);
return uart_tx(cfg->uart_dev, buf, len, timeout);
} else {
/* Mock the TX */
if (data->async_cb) {
struct uart_event evt;
evt.type = UART_TX_DONE;
evt.data.tx.buf = buf;
evt.data.tx.len = len;
data->async_cb(dev, &evt, data->async_cb_ctx);
}
return 0;
}
}
static int rs485_tx_abort(const struct device* dev) {
const struct rs485_config* cfg = dev->config;
return uart_tx_abort(cfg->uart_dev);
}
static int rs485_rx_buf_rsp(const struct device* dev, uint8_t* buf,
size_t len) {
const struct rs485_config* cfg = dev->config;
return uart_rx_buf_rsp(cfg->uart_dev, buf, len);
}
static int rs485_rx_enable(const struct device* dev, uint8_t* buf, size_t len,
int32_t timeout) {
const struct rs485_config* cfg = dev->config;
return uart_rx_enable(cfg->uart_dev, buf, len, timeout);
}
static int rs485_rx_disable(const struct device* dev) {
const struct rs485_config* cfg = dev->config;
return uart_rx_disable(cfg->uart_dev);
}
#endif /* CONFIG_UART_ASYNC_API */
static DEVICE_API(uart, rs485_api) = {
.poll_in = rs485_poll_in,
.poll_out = rs485_poll_out,
#ifdef CONFIG_UART_USE_RUNTIME_CONFIGURE
.config_get = rs485_config_get,
.configure = rs485_configure,
#endif /* CONFIG_UART_USE_RUNTIME_CONFIGURE */
.err_check = rs485_err_check,
#ifdef CONFIG_UART_INTERRUPT_DRIVEN
.fifo_fill = rs485_fifo_fill,
.fifo_read = rs485_fifo_read,
.irq_tx_enable = rs485_irq_tx_enable,
.irq_rx_enable = rs485_irq_rx_enable,
.irq_tx_disable = rs485_irq_tx_disable,
.irq_rx_disable = rs485_irq_rx_disable,
.irq_tx_ready = rs485_irq_tx_ready,
.irq_rx_ready = rs485_irq_rx_ready,
.irq_tx_complete = rs485_irq_tx_complete,
.irq_callback_set = rs485_irq_callback_set,
.irq_update = rs485_irq_update,
.irq_is_pending = rs485_irq_is_pending,
#endif /* CONFIG_UART_INTERRUPT_DRIVEN */
#ifdef CONFIG_UART_ASYNC_API
.callback_set = rs485_callback_set,
.tx = rs485_tx,
.tx_abort = rs485_tx_abort,
.rx_enable = rs485_rx_enable,
.rx_buf_rsp = rs485_rx_buf_rsp,
.rx_disable = rs485_rx_disable,
#endif /* CONFIG_UART_ASYNC_API */
};
static int rs485_init(const struct device* dev) {
const struct rs485_config* config = dev->config;
struct rs485_data* data = dev->data;
int ret;
k_event_init(&(data->timer_evt));
if (!device_is_ready(config->uart_dev)) {
LOG_ERR("DE GPIO not ready");
return -ENODEV;
}
if (!device_is_ready(config->de.port)) {
LOG_ERR("DE GPIO not ready");
return -ENODEV;
}
if (!device_is_ready(config->re.port)) {
LOG_ERR("RE GPIO not ready");
return -ENODEV;
}
ret = gpio_pin_configure_dt(&config->de, GPIO_OUTPUT);
if (ret < 0) {
LOG_ERR("Could not configure DE GPIO (%d)", ret);
return ret;
}
ret = gpio_pin_configure_dt(&config->re, GPIO_OUTPUT);
if (ret < 0) {
LOG_ERR("Could not configure RE GPIO (%d)", ret);
return ret;
}
ret = rs485_compute_delays(dev, NULL);
if (ret < 0) {
LOG_ERR("Could not compute half-character period (%d)", ret);
return ret;
} else {
data->halfchar = ret;
}
/* Set up the timer */
if (!device_is_ready(config->timer_dev)) {
LOG_ERR("RE/DE timer not ready");
return -ENODEV;
}
/* Put into RX mode */
return rs485_set_tx(dev, false);
}
#define DEFINE_WIDESKY_RS485(inst) \
static const struct rs485_config rs485_cfg_##inst = { \
.uart_dev = DEVICE_DT_GET(DT_INST_BUS(inst)), \
.timer_dev = DEVICE_DT_GET(DT_INST_PROP(inst, rs485_timer)), \
.de = GPIO_DT_SPEC_INST_GET(inst, rs485_de_gpios), \
.re = GPIO_DT_SPEC_INST_GET(inst, rs485_re_gpios), \
.de_assert \
= DT_INST_PROP_OR(inst, rs485_assertion_time_de_us, 0), \
.de_deassert \
= DT_INST_PROP_OR(inst, rs485_deassertion_time_de_us, 0), \
.re_assert \
= DT_INST_PROP_OR(inst, rs485_assertion_time_re_us, 0), \
.re_deassert \
= DT_INST_PROP_OR(inst, rs485_deassertion_time_re_us, 0), \
.port = DT_INST_PROP_OR(inst, port, 0), \
}; \
static struct rs485_data rs485_data_##inst = { \
.state = WSH_RS485_DIR_BOTH, \
IF_DISABLED(CONFIG_UART_USE_RUNTIME_CONFIGURE, \
(.baud_rate = DT_INST_PROP(inst, current_speed), )) \
.cts_delay \
= 7, \
.halfchar = 0, \
}; \
\
DEVICE_DT_INST_DEFINE(inst, rs485_init, NULL, &rs485_data_##inst, \
&rs485_cfg_##inst, PRE_KERNEL_1, \
CONFIG_SERIAL_INIT_PRIORITY, &rs485_api);
DT_INST_FOREACH_STATUS_OKAY(DEFINE_WIDESKY_RS485)
/*
* RS-485 flow control driver
* ©2025 WideSky.Cloud Pty Ltd
*/
#ifndef APP_DRIVERS_WSH_RS485_H_
#define APP_DRIVERS_WSH_RS485_H_
struct device;
#define WSH_RS485_DIR_NONE (0) /*!< Port is disabled */
#define WSH_RS485_DIR_RXO (1) /*!< Port is in receive-only mode */
#define WSH_RS485_DIR_TXO (2) /*!< Port is in transmit-only mode */
#define WSH_RS485_DIR_BOTH (3) /*!< Port is operating both ways */
/*!
* Callback for activity LED triggers
*/
typedef void(rs485_act_cb)(const struct device* dev, uint8_t dir, void* ctx);
/*!
* Set clear-to-send delay
*/
int rs485_cts_delay(const struct device* dev, uint8_t delay);
/*!
* Change the permitted directions on the port.
*/
int rs485_set_en_direction(const struct device* dev, uint8_t dir);
/*!
* Get the currently permitted direction.
*/
int rs485_get_en_direction(const struct device* dev);
/*!
* Get the currently active direction.
*/
int rs485_get_act_direction(const struct device* dev);
/*!
* Configure a callback for blinking LEDs on activity
*/
int rs485_set_activity_callback(const struct device* dev, rs485_act_cb* cb,
void* ctx);
#endif /* APP_DRIVERS_WSH_RS485_H_ */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment