blob: 84dacf5c253cfbc7f25210dd8e0ad67a46a3fd0a [file] [log] [blame]
/*
* Copyright (c) 2018, Oticon A/S
* Copyright (c) 2025, Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/devicetree.h>
#if DT_HAS_COMPAT_STATUS_OKAY(zephyr_native_posix_uart)
#define DT_DRV_COMPAT zephyr_native_posix_uart
#warning "zephyr,native-posix-uart is deprecated in favor of zephyr,native-pty-uart"
#else
#define DT_DRV_COMPAT zephyr_native_pty_uart
#endif
#include <stdbool.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/kernel.h>
#include <cmdline.h> /* native_sim command line options header */
#include <posix_native_task.h>
#include <nsi_host_trampolines.h>
#include <nsi_tracing.h>
#include "uart_native_pty_bottom.h"
#define ERROR posix_print_error_and_exit
#define WARN posix_print_warning
/*
* UART driver for native simulator based boards.
* It can support a configurable number of UARTs.
*
* One (and only one) of this can be connected to the process STDIN+STDOUT otherwise, they are
* connected to a dedicated pseudo terminal.
*
* Connecting to a dedicated PTY is the recommended option for interactive use, as the pseudo
* terminal driver will be configured in "raw" mode and will therefore behave more like a real UART.
*
* When connected to its own pseudo terminal, it may also auto attach a terminal emulator to it,
* if set so from command line.
*/
struct native_pty_status {
int out_fd; /* File descriptor used for output */
int in_fd; /* File descriptor used for input */
bool on_stdinout; /* This UART is connected to a PTY and not STDIN/OUT */
bool auto_attach; /* For PTY, attach a terminal emulator automatically */
char *auto_attach_cmd; /* If auto_attach, which command to launch the terminal emulator */
bool wait_pts; /* Hold writes to the uart/pts until a client is connected/ready */
bool cmd_request_stdinout; /* User requested to connect this UART to the stdin/out */
#ifdef CONFIG_UART_ASYNC_API
struct {
const struct device *dev;
struct k_work_delayable tx_done;
uart_callback_t user_callback;
void *user_data;
const uint8_t *tx_buf;
size_t tx_len;
uint8_t *rx_buf;
size_t rx_len;
/* Instance-specific RX thread. */
struct k_thread rx_thread;
/* Stack for RX thread */
K_KERNEL_STACK_MEMBER(rx_stack, CONFIG_ARCH_POSIX_RECOMMENDED_STACK_SIZE);
} async;
#endif /* CONFIG_UART_ASYNC_API */
};
static void np_uart_poll_out(const struct device *dev, unsigned char out_char);
static int np_uart_poll_in(const struct device *dev, unsigned char *p_char);
static int np_uart_init(const struct device *dev);
#ifdef CONFIG_UART_ASYNC_API
static void np_uart_tx_done_work(struct k_work *work);
static int np_uart_callback_set(const struct device *dev, uart_callback_t callback,
void *user_data);
static int np_uart_tx(const struct device *dev, const uint8_t *buf, size_t len, int32_t timeout);
static int np_uart_tx_abort(const struct device *dev);
static int np_uart_rx_buf_rsp(const struct device *dev, uint8_t *buf, size_t len);
static int np_uart_rx_enable(const struct device *dev, uint8_t *buf, size_t len, int32_t timeout);
static int np_uart_rx_disable(const struct device *dev);
#endif /* CONFIG_UART_ASYNC_API */
static DEVICE_API(uart, np_uart_driver_api) = {
.poll_out = np_uart_poll_out,
.poll_in = np_uart_poll_in,
#ifdef CONFIG_UART_ASYNC_API
.callback_set = np_uart_callback_set,
.tx = np_uart_tx,
.tx_abort = np_uart_tx_abort,
.rx_buf_rsp = np_uart_rx_buf_rsp,
.rx_enable = np_uart_rx_enable,
.rx_disable = np_uart_rx_disable,
#endif /* CONFIG_UART_ASYNC_API */
};
#define NATIVE_PTY_INSTANCE(inst) \
static struct native_pty_status native_pty_status_##inst; \
\
DEVICE_DT_INST_DEFINE(inst, np_uart_init, NULL, \
(void *)&native_pty_status_##inst, NULL, \
PRE_KERNEL_1, CONFIG_SERIAL_INIT_PRIORITY, \
&np_uart_driver_api);
DT_INST_FOREACH_STATUS_OKAY(NATIVE_PTY_INSTANCE);
/**
* @brief Initialize a native_pty serial port
*
* @param dev uart device struct
*
* @return 0 (if it fails catastrophically, the execution is terminated)
*/
static int np_uart_init(const struct device *dev)
{
static bool stdinout_used;
struct native_pty_status *d;
d = (struct native_pty_status *)dev->data;
if (IS_ENABLED(CONFIG_UART_NATIVE_PTY_0_ON_STDINOUT)) {
static bool first_node = true;
if (first_node) {
d->on_stdinout = true;
}
first_node = false;
}
if (d->cmd_request_stdinout) {
if (stdinout_used) {
nsi_print_warning("%s requested to connect to STDIN/OUT, but another UART"
" is already connected to it => ignoring request.\n",
dev->name);
} else {
d->on_stdinout = true;
}
}
if (d->on_stdinout == true) {
d->in_fd = np_uart_pty_get_stdin_fileno();
d->out_fd = np_uart_pty_get_stdout_fileno();
stdinout_used = true;
} else {
if (d->auto_attach_cmd == NULL) {
d->auto_attach_cmd = CONFIG_UART_NATIVE_PTY_AUTOATTACH_DEFAULT_CMD;
} else { /* Running with --attach_uart_cmd, implies --attach_uart */
d->auto_attach = true;
}
int tty_fn = np_uart_open_pty(dev->name, d->auto_attach_cmd, d->auto_attach,
d->wait_pts);
d->in_fd = tty_fn;
d->out_fd = tty_fn;
}
#ifdef CONFIG_UART_ASYNC_API
k_work_init_delayable(&d->async.tx_done, np_uart_tx_done_work);
d->async.dev = dev;
#endif
return 0;
}
/*
* @brief Output a character towards the serial port
*
* @param dev UART device struct
* @param out_char Character to send.
*/
static void np_uart_poll_out(const struct device *dev, unsigned char out_char)
{
int ret;
struct native_pty_status *d = (struct native_pty_status *)dev->data;
if (d->wait_pts) {
while (1) {
int rc = np_uart_slave_connected(d->out_fd);
if (rc == 1) {
break;
}
k_sleep(K_MSEC(100));
}
}
/* The return value of write() cannot be ignored (there is a warning)
* but we do not need the return value for anything.
*/
ret = nsi_host_write(d->out_fd, &out_char, 1);
(void) ret;
}
/**
* @brief Poll the device for input.
*
* @param dev UART device structure.
* @param p_char Pointer to character.
*
* @retval 0 If a character arrived and was stored in p_char
* @retval -1 If no character was available to read
*/
static int np_uart_stdin_poll_in(const struct device *dev, unsigned char *p_char)
{
int in_f = ((struct native_pty_status *)dev->data)->in_fd;
static bool disconnected;
int rc;
if (disconnected == true) {
return -1;
}
rc = np_uart_stdin_poll_in_bottom(in_f, p_char, 1);
if (rc == -2) {
disconnected = true;
}
return rc == 1 ? 0 : -1;
}
/**
* @brief Poll the device for input.
*
* @param dev UART device structure.
* @param p_char Pointer to character.
*
* @retval 0 If a character arrived and was stored in p_char
* @retval -1 If no character was available to read
*/
static int np_uart_pty_poll_in(const struct device *dev, unsigned char *p_char)
{
int n = -1;
int in_f = ((struct native_pty_status *)dev->data)->in_fd;
n = nsi_host_read(in_f, p_char, 1);
if (n == -1) {
return -1;
}
return 0;
}
static int np_uart_poll_in(const struct device *dev, unsigned char *p_char)
{
if (((struct native_pty_status *)dev->data)->on_stdinout) {
return np_uart_stdin_poll_in(dev, p_char);
} else {
return np_uart_pty_poll_in(dev, p_char);
}
}
#ifdef CONFIG_UART_ASYNC_API
static int np_uart_callback_set(const struct device *dev, uart_callback_t callback, void *user_data)
{
struct native_pty_status *data = dev->data;
data->async.user_callback = callback;
data->async.user_data = user_data;
return 0;
}
static void np_uart_tx_done_work(struct k_work *work)
{
struct k_work_delayable *dwork = k_work_delayable_from_work(work);
struct native_pty_status *data =
CONTAINER_OF(dwork, struct native_pty_status, async.tx_done);
struct uart_event evt;
unsigned int key = irq_lock();
evt.type = UART_TX_DONE;
evt.data.tx.buf = data->async.tx_buf;
evt.data.tx.len = data->async.tx_len;
(void)nsi_host_write(data->out_fd, evt.data.tx.buf, evt.data.tx.len);
data->async.tx_buf = NULL;
if (data->async.user_callback) {
data->async.user_callback(data->async.dev, &evt, data->async.user_data);
}
irq_unlock(key);
}
static int np_uart_tx(const struct device *dev, const uint8_t *buf, size_t len, int32_t timeout)
{
struct native_pty_status *data = dev->data;
if (data->async.tx_buf) {
/* Port is busy */
return -EBUSY;
}
data->async.tx_buf = buf;
data->async.tx_len = len;
/* Run the callback on the next tick to give the caller time to use the return value */
k_work_reschedule(&data->async.tx_done, K_TICKS(1));
return 0;
}
static int np_uart_tx_abort(const struct device *dev)
{
struct native_pty_status *data = dev->data;
struct k_work_sync sync;
struct uart_event evt;
bool not_idle;
/* Cancel the callback */
not_idle = k_work_cancel_delayable_sync(&data->async.tx_done, &sync);
if (!not_idle) {
return -EFAULT;
}
/* Generate TX_DONE event with number of bytes transmitted */
evt.type = UART_TX_DONE;
evt.data.tx.buf = data->async.tx_buf;
evt.data.tx.len = 0;
if (data->async.user_callback) {
data->async.user_callback(data->async.dev, &evt, data->async.user_data);
}
/* Reset state */
data->async.tx_buf = NULL;
return 0;
}
/*
* Emulate async interrupts using a polling thread
*/
static void native_pty_uart_async_poll_function(void *arg1, void *arg2, void *arg3)
{
const struct device *dev = arg1;
struct native_pty_status *data = dev->data;
struct uart_event evt;
int rc;
ARG_UNUSED(arg2);
ARG_UNUSED(arg3);
while (data->async.rx_len) {
rc = np_uart_stdin_poll_in_bottom(data->in_fd, data->async.rx_buf,
data->async.rx_len);
if (rc > 0) {
/* Data received */
evt.type = UART_RX_RDY;
evt.data.rx.buf = data->async.rx_buf;
evt.data.rx.offset = 0;
evt.data.rx.len = rc;
/* User callback */
if (data->async.user_callback) {
data->async.user_callback(data->async.dev, &evt,
data->async.user_data);
}
}
if ((data->async.rx_len != 0) && (rc < 0)) {
/* Sleep if RX not disabled and last read didn't result in any data */
k_sleep(K_MSEC(10));
}
}
}
static int np_uart_rx_buf_rsp(const struct device *dev, uint8_t *buf, size_t len)
{
/* Driver never requests additional buffers */
return -ENOTSUP;
}
static int np_uart_rx_enable(const struct device *dev, uint8_t *buf, size_t len, int32_t timeout)
{
struct native_pty_status *data = dev->data;
ARG_UNUSED(timeout);
if (data->async.rx_buf != NULL) {
return -EBUSY;
}
data->async.rx_buf = buf;
data->async.rx_len = len;
/* Create a thread which will wait for data - replacement for IRQ */
k_thread_create(&data->async.rx_thread, data->async.rx_stack,
K_KERNEL_STACK_SIZEOF(data->async.rx_stack),
native_pty_uart_async_poll_function,
(void *)dev, NULL, NULL,
K_HIGHEST_THREAD_PRIO, 0, K_NO_WAIT);
return 0;
}
static int np_uart_rx_disable(const struct device *dev)
{
struct native_pty_status *data = dev->data;
if (data->async.rx_buf == NULL) {
return -EFAULT;
}
data->async.rx_len = 0;
data->async.rx_buf = NULL;
/* Wait for RX thread to terminate */
return k_thread_join(&data->async.rx_thread, K_FOREVER);
}
#endif /* CONFIG_UART_ASYNC_API */
#define NATIVE_PTY_SET_AUTO_ATTACH_CMD(inst, cmd) \
native_pty_status_##inst.auto_attach_cmd = cmd;
#define NATIVE_PTY_SET_AUTO_ATTACH(inst, value) \
native_pty_status_##inst.auto_attach = value;
#define NATIVE_PTY_SET_WAIT_PTS(inst, value) \
native_pty_status_##inst.wait_pts = value;
static void auto_attach_cmd_cb(char *argv, int offset)
{
DT_INST_FOREACH_STATUS_OKAY_VARGS(NATIVE_PTY_SET_AUTO_ATTACH_CMD, &argv[offset]);
DT_INST_FOREACH_STATUS_OKAY_VARGS(NATIVE_PTY_SET_AUTO_ATTACH, true);
}
static void auto_attach_cb(char *argv, int offset)
{
DT_INST_FOREACH_STATUS_OKAY_VARGS(NATIVE_PTY_SET_AUTO_ATTACH, true);
}
static void wait_pts_cb(char *argv, int offset)
{
DT_INST_FOREACH_STATUS_OKAY_VARGS(NATIVE_PTY_SET_WAIT_PTS, true);
}
#define INST_NAME(inst) DEVICE_DT_NAME(DT_DRV_INST(inst))
#define NATIVE_PTY_COMMAND_LINE_OPTS(inst) \
{ \
.is_switch = true, \
.option = INST_NAME(inst) "_stdinout", \
.type = 'b', \
.dest = &native_pty_status_##inst.cmd_request_stdinout, \
.descript = "Connect "INST_NAME(inst)" to STDIN/OUT instead of a PTY" \
" (can only be done for one UART)" \
}, \
{ \
.is_switch = true, \
.option = INST_NAME(inst) "_attach_uart", \
.type = 'b', \
.dest = &native_pty_status_##inst.auto_attach, \
.descript = "Automatically attach "INST_NAME(inst)" to a terminal emulator." \
" (only applicable when connected to PTYs)" \
}, \
{ \
.option = INST_NAME(inst) "_attach_uart_cmd", \
.name = "\"cmd\"", \
.type = 's', \
.dest = &native_pty_status_##inst.auto_attach_cmd, \
.descript = "Command used to automatically attach to the terminal "INST_NAME(inst) \
" (implies "INST_NAME(inst)"_auto_attach), by default: " \
"'" CONFIG_UART_NATIVE_PTY_AUTOATTACH_DEFAULT_CMD "'" \
" (only applicable when connected to PTYs)" \
}, \
{ \
.is_switch = true, \
.option = INST_NAME(inst) "_wait_uart", \
.type = 'b', \
.dest = &native_pty_status_##inst.wait_pts, \
.descript = "Hold writes to "INST_NAME(inst)" until a client is connected/ready" \
" (only applicable when connected to PTYs)" \
},
static void np_add_uart_options(void)
{
static struct args_struct_t uart_options[] = {
/* Set of parameters that apply to all PTY UARTs: */
{
.is_switch = true,
.option = "attach_uart",
.type = 'b',
.call_when_found = auto_attach_cb,
.descript = "Automatically attach all PTY UARTs to a terminal emulator."
" (only applicable when connected to PTYs)"
},
{
.option = "attach_uart_cmd",
.name = "\"cmd\"",
.type = 's',
.call_when_found = auto_attach_cmd_cb,
.descript = "Command used to automatically attach all PTY UARTs to a terminal "
"emulator (implies auto_attach), by default: "
"'" CONFIG_UART_NATIVE_PTY_AUTOATTACH_DEFAULT_CMD "'"
" (only applicable when connected to PTYs)"
},
{
.is_switch = true,
.option = "wait_uart",
.type = 'b',
.call_when_found = wait_pts_cb,
.descript = "Hold writes to all PTY UARTs until a client is connected/ready"
" (only applicable when connected to PTYs)"
},
/* Set of parameters that apply to each individual PTY UART: */
DT_INST_FOREACH_STATUS_OKAY(NATIVE_PTY_COMMAND_LINE_OPTS)
ARG_TABLE_ENDMARKER
};
native_add_command_line_opts(uart_options);
}
#define NATIVE_PTY_CLEANUP(inst) \
if ((!native_pty_status_##inst.on_stdinout) && (native_pty_status_##inst.in_fd != 0)) { \
nsi_host_close(native_pty_status_##inst.in_fd); \
native_pty_status_##inst.in_fd = 0; \
}
static void np_cleanup_uart(void)
{
DT_INST_FOREACH_STATUS_OKAY(NATIVE_PTY_CLEANUP);
}
NATIVE_TASK(np_add_uart_options, PRE_BOOT_1, 11);
NATIVE_TASK(np_cleanup_uart, ON_EXIT, 99);