Files
blinky/src/usb_cdc_module.c

650 lines
16 KiB
C
Raw Permalink Normal View History

#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <app_event_manager.h>
#define MODULE usb_cdc_module
#include <caf/events/module_state_event.h>
#include <caf/events/power_event.h>
#include <zephyr/device.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 "module_lifecycle.h"
#include "proto_rx_event.h"
#include "proto_transport_state_event.h"
#include "proto_tx_event.h"
#include "usb_control_event.h"
#include "usb_state_event.h"
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
#define USB_CDC_RX_RING_BUF_SIZE 256
#define USB_CDC_TX_RING_BUF_SIZE 256
#define USB_CDC_RX_CHUNK_SIZE 32
#define USB_CDC_PROTO_RX_BUF_SIZE PROTO_MAX_FRAME_LEN
#define USB_CDC_EXPECTED_BAUDRATE 115200U
enum usb_cdc_business_state {
USB_CDC_BUS_OFFLINE = 0,
USB_CDC_WAIT_DTR,
USB_CDC_SESSION_READY,
};
struct usb_cdc_ctx {
struct module_lifecycle_ctx lc;
enum usb_cdc_business_state business;
const struct device *cdc_dev;
uint8_t rx_ring_buffer[USB_CDC_RX_RING_BUF_SIZE];
uint8_t tx_ring_buffer[USB_CDC_TX_RING_BUF_SIZE];
struct ring_buf rx_ringbuf;
struct ring_buf tx_ringbuf;
struct k_work rx_work;
uint8_t proto_rx_buf[USB_CDC_PROTO_RX_BUF_SIZE];
bool usb_active;
size_t proto_rx_len;
};
static int do_init(void);
static int do_start(void);
static int do_stop(void);
static const struct module_lifecycle_cfg lifecycle_cfg = {
.mode = ML_MODE_POWER,
.stopped_state = MODULE_STATE_STANDBY,
};
static const struct module_lifecycle_ops lifecycle_ops = {
.do_init = do_init,
.do_start = do_start,
.do_stop = do_stop,
};
static struct usb_cdc_ctx ctx = {
.lc = {
.state = LC_UNINIT,
.cfg = &lifecycle_cfg,
.ops = &lifecycle_ops,
},
.business = USB_CDC_BUS_OFFLINE,
.cdc_dev = DEVICE_DT_GET(DT_NODELABEL(cdc_acm_uart0)),
.usb_active = false,
.proto_rx_len = 0U,
};
static void validate_line_coding(void);
static const char *usb_cdc_business_state_name(enum usb_cdc_business_state state)
{
switch (state) {
case USB_CDC_BUS_OFFLINE:
return "BUS_OFFLINE";
case USB_CDC_WAIT_DTR:
return "WAIT_DTR";
case USB_CDC_SESSION_READY:
return "SESSION_READY";
default:
return "?";
}
}
static const char *proto_link_state_name(enum proto_transport_link_state state)
{
switch (state) {
case PROTO_TRANSPORT_LINK_DOWN:
return "DOWN";
case PROTO_TRANSPORT_LINK_READY:
return "READY";
default:
return "?";
}
}
static bool lifecycle_is_ready(void)
{
return module_lifecycle_is_running(&ctx.lc);
}
static enum proto_transport_link_state transport_link_state_get(void)
{
return (lifecycle_is_ready() &&
(ctx.business == USB_CDC_SESSION_READY)) ?
PROTO_TRANSPORT_LINK_READY :
PROTO_TRANSPORT_LINK_DOWN;
}
static void reset_ring_buffers(void)
{
unsigned int key = irq_lock();
ring_buf_init(&ctx.rx_ringbuf, sizeof(ctx.rx_ring_buffer), ctx.rx_ring_buffer);
ring_buf_init(&ctx.tx_ringbuf, sizeof(ctx.tx_ring_buffer), ctx.tx_ring_buffer);
irq_unlock(key);
}
static void disable_uart_io(void)
{
uart_irq_rx_disable(ctx.cdc_dev);
uart_irq_tx_disable(ctx.cdc_dev);
ctx.proto_rx_len = 0U;
reset_ring_buffers();
}
static void state_reconcile(enum module_lifecycle old_lifecycle,
enum usb_cdc_business_state old_business)
{
enum proto_transport_link_state old_link =
((old_lifecycle == LC_RUNNING) &&
(old_business == USB_CDC_SESSION_READY)) ?
PROTO_TRANSPORT_LINK_READY :
PROTO_TRANSPORT_LINK_DOWN;
enum proto_transport_link_state new_link = transport_link_state_get();
if ((old_lifecycle == LC_RUNNING) &&
(old_business == USB_CDC_SESSION_READY) &&
(new_link == PROTO_TRANSPORT_LINK_DOWN)) {
LOG_INF("CDC reconcile: link READY -> DOWN, disable UART IO");
disable_uart_io();
}
if ((old_link == PROTO_TRANSPORT_LINK_DOWN) &&
(new_link == PROTO_TRANSPORT_LINK_READY)) {
int err;
LOG_INF("CDC reconcile: link DOWN -> READY (business:%s usb_active:%u lc:%s)",
usb_cdc_business_state_name(ctx.business), ctx.usb_active,
module_lifecycle_name(ctx.lc.state));
validate_line_coding();
err = uart_line_ctrl_set(ctx.cdc_dev, UART_LINE_CTRL_DCD, 1);
if (err) {
LOG_WRN("Failed to set DCD (%d)", err);
}
err = uart_line_ctrl_set(ctx.cdc_dev, UART_LINE_CTRL_DSR, 1);
if (err) {
LOG_WRN("Failed to set DSR (%d)", err);
}
uart_irq_rx_enable(ctx.cdc_dev);
}
if (old_link != new_link) {
LOG_INF("CDC link %s -> %s (business:%s usb_active:%u lc:%s)",
proto_link_state_name(old_link),
proto_link_state_name(new_link),
usb_cdc_business_state_name(ctx.business), ctx.usb_active,
module_lifecycle_name(ctx.lc.state));
submit_proto_transport_state_event(PROTO_TRANSPORT_USB_CDC,
new_link);
}
}
static void business_state_set(enum usb_cdc_business_state new_state)
{
enum module_lifecycle old_lifecycle = ctx.lc.state;
enum usb_cdc_business_state old_business = ctx.business;
if (ctx.business == new_state) {
return;
}
ctx.business = new_state;
LOG_INF("CDC business %s -> %s (usb_active:%u lc:%s)",
usb_cdc_business_state_name(old_business),
usb_cdc_business_state_name(ctx.business), ctx.usb_active,
module_lifecycle_name(ctx.lc.state));
state_reconcile(old_lifecycle, old_business);
}
static void business_state_sync_from_usb(void)
{
LOG_INF("CDC sync from USB (usb_active:%u business:%s lc:%s)",
ctx.usb_active, usb_cdc_business_state_name(ctx.business),
module_lifecycle_name(ctx.lc.state));
if (!ctx.usb_active) {
business_state_set(USB_CDC_BUS_OFFLINE);
return;
}
if (!lifecycle_is_ready()) {
return;
}
if (ctx.business == USB_CDC_BUS_OFFLINE) {
business_state_set(USB_CDC_WAIT_DTR);
}
}
static void kick_tx(void)
{
if (transport_link_state_get() != PROTO_TRANSPORT_LINK_READY) {
LOG_INF("CDC kick_tx skipped: link=%s business:%s usb_active:%u tx_rb:%u",
proto_link_state_name(transport_link_state_get()),
usb_cdc_business_state_name(ctx.business), ctx.usb_active,
ring_buf_size_get(&ctx.tx_ringbuf));
return;
}
LOG_INF("CDC kick_tx: enable TX IRQ (tx_rb:%u)", ring_buf_size_get(&ctx.tx_ringbuf));
uart_irq_tx_enable(ctx.cdc_dev);
}
static void validate_line_coding(void)
{
uint32_t baudrate = 0U;
int err;
err = uart_line_ctrl_get(ctx.cdc_dev, UART_LINE_CTRL_BAUD_RATE, &baudrate);
if (err) {
LOG_WRN("Failed to get CDC baudrate (%d)", err);
} else {
LOG_INF("CDC baudrate %u", baudrate);
if (baudrate != USB_CDC_EXPECTED_BAUDRATE) {
LOG_WRN("Expected CDC baudrate %u, got %u",
USB_CDC_EXPECTED_BAUDRATE, baudrate);
}
}
#ifdef CONFIG_UART_USE_RUNTIME_CONFIGURE
{
struct uart_config cfg;
err = uart_config_get(ctx.cdc_dev, &cfg);
if (err) {
LOG_WRN("uart_config_get failed (%d)", err);
} else {
LOG_INF("CDC line coding data:%u stop:%u parity:%u flow:%u",
cfg.data_bits, cfg.stop_bits, cfg.parity,
cfg.flow_ctrl);
if ((cfg.data_bits != UART_CFG_DATA_BITS_8) ||
(cfg.stop_bits != UART_CFG_STOP_BITS_1) ||
(cfg.parity != UART_CFG_PARITY_NONE) ||
(cfg.flow_ctrl != UART_CFG_FLOW_CTRL_NONE)) {
LOG_WRN("Expected CDC line coding 115200 8N1 no flow control");
}
}
}
#endif
}
static bool try_extract_frame(void)
{
uint8_t *buf = ctx.proto_rx_buf;
size_t frame_len;
uint16_t magic;
if (ctx.proto_rx_len < PROTO_FRAME_HEADER_SIZE) {
return false;
}
magic = (uint16_t)buf[0] | ((uint16_t)buf[1] << 8);
if (magic != PROTO_FRAME_MAGIC) {
LOG_WRN("CDC invalid frame magic 0x%04x", magic);
memmove(buf, &buf[1], ctx.proto_rx_len - 1U);
ctx.proto_rx_len--;
return true;
}
if (buf[2] > PROTO_MAX_PAYLOAD_LEN) {
LOG_WRN("CDC invalid frame len:%u", buf[2]);
memmove(buf, &buf[1], ctx.proto_rx_len - 1U);
ctx.proto_rx_len--;
return true;
}
frame_len = PROTO_FRAME_HEADER_SIZE + buf[2];
if (ctx.proto_rx_len < frame_len) {
return false;
}
LOG_INF("CDC submit framed proto_rx len:%u", (unsigned int)frame_len);
(void)submit_proto_rx_event(PROTO_TRANSPORT_USB_CDC, buf, frame_len);
if (ctx.proto_rx_len > frame_len) {
memmove(buf, &buf[frame_len], ctx.proto_rx_len - frame_len);
}
ctx.proto_rx_len -= frame_len;
return true;
}
static void rx_work_handler(struct k_work *work)
{
uint8_t buffer[USB_CDC_RX_CHUNK_SIZE];
ARG_UNUSED(work);
while (true) {
uint32_t len;
unsigned int key = irq_lock();
len = ring_buf_get(&ctx.rx_ringbuf, buffer, sizeof(buffer));
irq_unlock(key);
if (len == 0U) {
break;
}
LOG_INF("CDC rx_work pulled %u bytes from RX ring (proto_rx_len:%u)",
(unsigned int)len, (unsigned int)ctx.proto_rx_len);
if ((ctx.proto_rx_len + len) > sizeof(ctx.proto_rx_buf)) {
LOG_WRN("Drop oversized CDC framed data len:%u",
(uint32_t)(ctx.proto_rx_len + len));
ctx.proto_rx_len = 0U;
}
if (len > 0U) {
memcpy(&ctx.proto_rx_buf[ctx.proto_rx_len], buffer, len);
ctx.proto_rx_len += len;
}
}
while (try_extract_frame()) {
}
}
static void cdc_interrupt_handler(const struct device *dev, void *user_data)
{
ARG_UNUSED(user_data);
while (uart_irq_update(dev) && uart_irq_is_pending(dev)) {
if (uart_irq_rx_ready(dev)) {
uint8_t buffer[USB_CDC_RX_CHUNK_SIZE];
int recv_len = uart_fifo_read(dev, buffer, sizeof(buffer));
if (recv_len < 0) {
LOG_ERR("Failed to read CDC RX FIFO");
continue;
}
if (recv_len > 0) {
uint32_t written;
unsigned int key = irq_lock();
written = ring_buf_put(&ctx.rx_ringbuf, buffer,
(uint32_t)recv_len);
irq_unlock(key);
if (written < (uint32_t)recv_len) {
LOG_WRN("Drop %d CDC RX bytes",
recv_len - (int)written);
}
LOG_INF("CDC IRQ RX recv:%d written:%u rx_rb:%u",
recv_len, (unsigned int)written,
ring_buf_size_get(&ctx.rx_ringbuf));
k_work_submit(&ctx.rx_work);
}
}
if (uart_irq_tx_ready(dev)) {
uint8_t buffer[USB_CDC_RX_CHUNK_SIZE];
uint32_t len;
int sent_len;
unsigned int key = irq_lock();
len = ring_buf_get(&ctx.tx_ringbuf, buffer, sizeof(buffer));
irq_unlock(key);
if (len == 0U) {
LOG_INF("CDC IRQ TX ready but no pending data, disable TX IRQ");
uart_irq_tx_disable(dev);
continue;
}
sent_len = uart_fifo_fill(dev, buffer, len);
if (sent_len < 0) {
LOG_ERR("Failed to write CDC TX FIFO");
uart_irq_tx_disable(dev);
} else if ((uint32_t)sent_len < len) {
LOG_WRN("Drop %u CDC TX bytes",
(unsigned int)(len - (uint32_t)sent_len));
}
LOG_INF("CDC IRQ TX requested:%u sent:%d tx_rb_remain:%u",
(unsigned int)len, sent_len,
ring_buf_size_get(&ctx.tx_ringbuf));
}
}
}
static int do_init(void)
{
if (!device_is_ready(ctx.cdc_dev)) {
LOG_ERR("CDC ACM device not ready");
return -ENODEV;
}
reset_ring_buffers();
k_work_init(&ctx.rx_work, rx_work_handler);
uart_irq_callback_set(ctx.cdc_dev, cdc_interrupt_handler);
ctx.business = USB_CDC_BUS_OFFLINE;
ctx.usb_active = false;
ctx.proto_rx_len = 0U;
LOG_INF("CDC init done (business:%s usb_active:%u lc:%s dev:%p)",
usb_cdc_business_state_name(ctx.business), ctx.usb_active,
module_lifecycle_name(ctx.lc.state), ctx.cdc_dev);
return 0;
}
static int do_start(void)
{
return 0;
}
static int do_stop(void)
{
LOG_INF("CDC stop requested (business:%s usb_active:%u lc:%s)",
usb_cdc_business_state_name(ctx.business), ctx.usb_active,
module_lifecycle_name(ctx.lc.state));
ctx.business = USB_CDC_BUS_OFFLINE;
return 0;
}
static int apply_lifecycle(enum module_lifecycle target)
{
enum module_lifecycle old_lifecycle = ctx.lc.state;
enum usb_cdc_business_state old_business = ctx.business;
int err = module_set_lifecycle(&ctx.lc, target);
if (err) {
return err;
}
state_reconcile(old_lifecycle, old_business);
if (target == LC_RUNNING) {
business_state_sync_from_usb();
}
LOG_INF("CDC lifecycle %s -> %s done (business:%s usb_active:%u link:%s)",
module_lifecycle_name(old_lifecycle), module_lifecycle_name(target),
usb_cdc_business_state_name(ctx.business), ctx.usb_active,
proto_link_state_name(transport_link_state_get()));
return 0;
}
static bool handle_usb_state_event(const struct usb_state_event *event)
{
bool new_usb_active = (event->state == USB_STATE_ACTIVE);
if (new_usb_active == ctx.usb_active) {
return false;
}
ctx.usb_active = new_usb_active;
LOG_INF("CDC usb_active -> %u from usb_state:%d (business:%s lc:%s)",
ctx.usb_active, event->state,
usb_cdc_business_state_name(ctx.business),
module_lifecycle_name(ctx.lc.state));
business_state_sync_from_usb();
return false;
}
static bool handle_usb_control_event(const struct usb_control_event *event)
{
if (event->dev != ctx.cdc_dev) {
return false;
}
switch (event->type) {
case USB_CONTROL_EVENT_CDC_LINE_STATE:
LOG_INF("CDC control event line_state dtr:%u usb_active:%u lc:%s business:%s",
event->data.cdc_line_state.dtr, ctx.usb_active,
module_lifecycle_name(ctx.lc.state),
usb_cdc_business_state_name(ctx.business));
if (!ctx.usb_active || !lifecycle_is_ready()) {
LOG_INF("CDC line_state ignored: usb_active:%u lc:%s",
ctx.usb_active, module_lifecycle_name(ctx.lc.state));
return false;
}
if (event->data.cdc_line_state.dtr) {
LOG_INF("CDC DTR set");
business_state_set(USB_CDC_SESSION_READY);
kick_tx();
} else {
LOG_INF("CDC DTR cleared");
business_state_set(USB_CDC_WAIT_DTR);
}
return false;
case USB_CONTROL_EVENT_CDC_LINE_CODING:
LOG_INF("CDC control event line_coding baud:%u data:%u stop:%u parity:%u flow:%u business:%s",
event->data.cdc_line_coding.baudrate,
event->data.cdc_line_coding.data_bits,
event->data.cdc_line_coding.stop_bits,
event->data.cdc_line_coding.parity,
event->data.cdc_line_coding.flow_ctrl,
usb_cdc_business_state_name(ctx.business));
if (event->data.cdc_line_coding.baudrate != 0U) {
LOG_INF("CDC baudrate %u",
event->data.cdc_line_coding.baudrate);
if (event->data.cdc_line_coding.baudrate !=
USB_CDC_EXPECTED_BAUDRATE) {
LOG_WRN("Expected CDC baudrate %u, got %u",
USB_CDC_EXPECTED_BAUDRATE,
event->data.cdc_line_coding.baudrate);
}
}
#ifdef CONFIG_UART_USE_RUNTIME_CONFIGURE
if ((event->data.cdc_line_coding.data_bits != 0U) ||
(event->data.cdc_line_coding.stop_bits != 0U) ||
(event->data.cdc_line_coding.parity != 0U) ||
(event->data.cdc_line_coding.flow_ctrl != 0U)) {
LOG_INF("CDC line coding data:%u stop:%u parity:%u flow:%u",
event->data.cdc_line_coding.data_bits,
event->data.cdc_line_coding.stop_bits,
event->data.cdc_line_coding.parity,
event->data.cdc_line_coding.flow_ctrl);
}
#endif
return false;
default:
return false;
}
}
static bool handle_proto_tx_event(const struct proto_tx_event *event)
{
uint32_t written;
unsigned int key;
if (event->transport != PROTO_TRANSPORT_USB_CDC) {
return false;
}
if (transport_link_state_get() != PROTO_TRANSPORT_LINK_READY) {
LOG_INF("CDC proto_tx ignored len:%u link:%s business:%s usb_active:%u",
(unsigned int)event->dyndata.size,
proto_link_state_name(transport_link_state_get()),
usb_cdc_business_state_name(ctx.business), ctx.usb_active);
return false;
}
key = irq_lock();
written = ring_buf_put(&ctx.tx_ringbuf, event->dyndata.data,
(uint32_t)event->dyndata.size);
irq_unlock(key);
LOG_INF("CDC proto_tx queued len:%u written:%u tx_rb:%u",
(unsigned int)event->dyndata.size, (unsigned int)written,
ring_buf_size_get(&ctx.tx_ringbuf));
if (written < event->dyndata.size) {
LOG_WRN("Drop %zu CDC TX bytes", event->dyndata.size - written);
}
if (written > 0U) {
kick_tx();
}
return false;
}
static bool app_event_handler(const struct app_event_header *aeh)
{
if (is_usb_state_event(aeh)) {
return handle_usb_state_event(cast_usb_state_event(aeh));
}
if (is_proto_tx_event(aeh)) {
return handle_proto_tx_event(cast_proto_tx_event(aeh));
}
if (is_usb_control_event(aeh)) {
return handle_usb_control_event(cast_usb_control_event(aeh));
}
if (is_module_state_event(aeh)) {
const struct module_state_event *event =
cast_module_state_event(aeh);
if (check_state(event, MODULE_ID(main), MODULE_STATE_READY)) {
(void)apply_lifecycle(LC_RUNNING);
}
return false;
}
if (is_power_down_event(aeh)) {
if (module_lifecycle_is_initialized(&ctx.lc)) {
(void)apply_lifecycle(LC_STOPPED);
}
return false;
}
if (is_wake_up_event(aeh)) {
if (module_lifecycle_is_initialized(&ctx.lc)) {
(void)apply_lifecycle(LC_RUNNING);
}
return false;
}
return false;
}
APP_EVENT_LISTENER(MODULE, app_event_handler);
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);
APP_EVENT_SUBSCRIBE(MODULE, proto_tx_event);
APP_EVENT_SUBSCRIBE(MODULE, usb_control_event);
APP_EVENT_SUBSCRIBE(MODULE, usb_state_event);
APP_EVENT_SUBSCRIBE_EARLY(MODULE, power_down_event);
APP_EVENT_SUBSCRIBE(MODULE, wake_up_event);