Created
September 18, 2025 02:23
-
-
Save sjlongland/4421dbf313f6abf814ce60284e4759ba to your computer and use it in GitHub Desktop.
RS-485 on Zephyr
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| &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>; | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * 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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * 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