From 70bd1b28e30e7c69bca58f44b950a260a4f81bf1 Mon Sep 17 00:00:00 2001 From: Xudong Zheng <7pkvm5aw@slicealias.com> Date: Sat, 23 Dec 2023 15:38:44 -0500 Subject: [PATCH] feat(split): wired split over serial RX support (WIP 2024-05-26) --- app/include/zmk/split/serial/serial.h | 28 ++++ app/src/split/CMakeLists.txt | 18 ++- app/src/split/Kconfig | 9 +- app/src/split/serial/CMakeLists.txt | 8 ++ app/src/split/serial/Kconfig | 20 +++ app/src/split/serial/central.c | 59 ++++++++ app/src/split/serial/serial.c | 199 ++++++++++++++++++++++++++ 7 files changed, 332 insertions(+), 9 deletions(-) create mode 100644 app/include/zmk/split/serial/serial.h create mode 100644 app/src/split/serial/CMakeLists.txt create mode 100644 app/src/split/serial/Kconfig create mode 100644 app/src/split/serial/central.c create mode 100644 app/src/split/serial/serial.c diff --git a/app/include/zmk/split/serial/serial.h b/app/include/zmk/split/serial/serial.h new file mode 100644 index 000000000000..30487d4410b5 --- /dev/null +++ b/app/include/zmk/split/serial/serial.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include + +// The serial protocol is defined for payloads of up to 254 bytes. This should +// be large enough to ensure that one message can be fully buffered. +#define SERIAL_BUF_SIZE 300 + +struct serial_device { + const struct device *dev; + uint8_t rx_buf[SERIAL_BUF_SIZE], tx_buf[SERIAL_BUF_SIZE]; + struct ring_buf rx_rb, tx_rb; + struct k_work rx_work; + +#ifdef CONFIG_ZMK_SPLIT_SERIAL_UART_POLL + bool poll; + struct k_work tx_work; + struct k_timer rx_timer; +#endif +}; + +void serial_handle_rx(uint32_t cmd, uint8_t *data, uint8_t len); diff --git a/app/src/split/CMakeLists.txt b/app/src/split/CMakeLists.txt index b59533d4d89f..e73bb6ab29d6 100644 --- a/app/src/split/CMakeLists.txt +++ b/app/src/split/CMakeLists.txt @@ -1,15 +1,23 @@ # Copyright (c) 2022 The ZMK Contributors # SPDX-License-Identifier: MIT -if (CONFIG_ZMK_SPLIT_BLE AND (NOT CONFIG_ZMK_SPLIT_ROLE_CENTRAL)) - target_sources(app PRIVATE listener.c) - target_sources(app PRIVATE service.c) +if (NOT CONFIG_ZMK_SPLIT_ROLE_CENTRAL) + if (CONFIG_ZMK_SPLIT_BLE OR CONFIG_ZMK_SPLIT_SERIAL) + target_sources(app PRIVATE listener.c) + target_sources(app PRIVATE service.c) + endif() endif() -if (CONFIG_ZMK_SPLIT_BLE AND CONFIG_ZMK_SPLIT_ROLE_CENTRAL) - target_sources(app PRIVATE central.c) +if (CONFIG_ZMK_SPLIT_ROLE_CENTRAL) + if (CONFIG_ZMK_SPLIT_BLE OR CONFIG_ZMK_SPLIT_SERIAL) + target_sources(app PRIVATE central.c) + endif() endif() if (CONFIG_ZMK_SPLIT_BLE) add_subdirectory(bluetooth) endif() + +if (CONFIG_ZMK_SPLIT_SERIAL) + add_subdirectory(serial) +endif() diff --git a/app/src/split/Kconfig b/app/src/split/Kconfig index 2950bce558b8..2c297800ebb2 100644 --- a/app/src/split/Kconfig +++ b/app/src/split/Kconfig @@ -49,16 +49,16 @@ config ZMK_SPLIT_PERIPHERAL_POSITION_QUEUE_SIZE endif #!ZMK_SPLIT_ROLE_CENTRAL -choice ZMK_SPLIT_TRANSPORT - prompt "Split transport" - config ZMK_SPLIT_BLE bool "BLE" + default ZMK_SPLIT && ZMK_BLE depends on ZMK_BLE select BT_USER_PHY_UPDATE select BT_AUTO_PHY_UPDATE -endchoice +config ZMK_SPLIT_SERIAL + bool "Serial" + select RING_BUFFER config ZMK_SPLIT_PERIPHERAL_HID_INDICATORS bool "Peripheral HID Indicators" @@ -70,3 +70,4 @@ config ZMK_SPLIT_PERIPHERAL_HID_INDICATORS endif rsource "bluetooth/Kconfig" +rsource "serial/Kconfig" diff --git a/app/src/split/serial/CMakeLists.txt b/app/src/split/serial/CMakeLists.txt new file mode 100644 index 000000000000..230d423565aa --- /dev/null +++ b/app/src/split/serial/CMakeLists.txt @@ -0,0 +1,8 @@ +# Copyright (c) 2023 The ZMK Contributors +# SPDX-License-Identifier: MIT + +if (CONFIG_ZMK_SPLIT_ROLE_CENTRAL) + target_sources(app PRIVATE central.c) +endif() + +target_sources(app PRIVATE serial.c) diff --git a/app/src/split/serial/Kconfig b/app/src/split/serial/Kconfig new file mode 100644 index 000000000000..3751ab6ebce0 --- /dev/null +++ b/app/src/split/serial/Kconfig @@ -0,0 +1,20 @@ +if ZMK_SPLIT && ZMK_SPLIT_SERIAL + +menu "Serial Transport" + +config ZMK_SPLIT_SERIAL_UART + bool "Serial over UART" + select CRC + default y + +config ZMK_SPLIT_SERIAL_UART_POLL + bool "Serial over UART Polling API" + default DT_HAS_RASPBERRYPI_PICO_UART_PIO_ENABLED || BOARD_NATIVE_SIM + +config ZMK_SPLIT_SERIAL_CDC_ACM + bool "Serial over USB CDC ACM" + default n + +endmenu + +endif diff --git a/app/src/split/serial/central.c b/app/src/split/serial/central.c new file mode 100644 index 000000000000..42f372b4412d --- /dev/null +++ b/app/src/split/serial/central.c @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +#include +#include +#include + +// TODO TODO TODO +#include +LOG_MODULE_DECLARE(slicemk); + +#define POSITION_STATE_DATA_LEN 16 +static uint8_t position_state[POSITION_STATE_DATA_LEN]; +static uint8_t changed_positions[POSITION_STATE_DATA_LEN]; + +static void serial_handle_bitmap(uint8_t *data, uint8_t len) { + for (int i = 0; i < POSITION_STATE_DATA_LEN; i++) { + changed_positions[i] = ((uint8_t *)data)[i] ^ position_state[i]; + position_state[i] = ((uint8_t *)data)[i]; + LOG_DBG("TODO TODO TODO data: %d", position_state[i]); + } + + for (int i = 0; i < POSITION_STATE_DATA_LEN; i++) { + for (int j = 0; j < 8; j++) { + if (changed_positions[i] & BIT(j)) { + uint32_t position = (i * 8) + j; + bool pressed = position_state[i] & BIT(j); + + // TODO TODO TODO does zero make sense? check ble central. what + // slot is central itself? + int slot = 0; + + struct zmk_position_state_changed ev = {.source = slot, + .position = position, + .state = pressed, + .timestamp = k_uptime_get()}; + zmk_position_state_change_handle(&ev); + } + } + } +} + +void serial_handle_rx(uint32_t cmd, uint8_t *data, uint8_t len) { + switch (cmd) { + // Handle split bitmap transformed (sbt) version 0. + case 0x73627400: + serial_handle_bitmap(data, len); + break; + + default: + LOG_ERR("Received unexpected UART command 0x%08x", cmd); + break; + } +} diff --git a/app/src/split/serial/serial.c b/app/src/split/serial/serial.c new file mode 100644 index 000000000000..a4876b60822d --- /dev/null +++ b/app/src/split/serial/serial.c @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include + +#include + +// TODO TODO TODO +#include +LOG_MODULE_REGISTER(slicemk); + +#define SERIAL_MSG_PREFIX "UarT" + +K_THREAD_STACK_DEFINE(serial_wq_stack, 1024); +static struct k_work_q serial_wq; + +static struct serial_device serial_devs[] = { +#ifdef CONFIG_ZMK_SPLIT_SERIAL_UART + { + .dev = DEVICE_DT_GET(DT_CHOSEN(zmk_split_uart)), +#ifdef CONFIG_ZMK_SPLIT_SERIAL_UART_POLL + .poll = true, +#endif + }, +#endif +#ifdef CONFIG_ZMK_SPLIT_SERIAL_CDC_ACM + { + .dev = DEVICE_DT_GET(DT_CHOSEN(zmk_split_cdc_acm)), + }, +#endif +}; + +#define CONFIG_ZMK_SPLIT_SERIAL_COUNT ARRAY_SIZE(serial_devs) + +static bool serial_tx_callback(struct serial_device *ud) { + // Read data from buffer. Stop transmitting if buffer is empty. + uint8_t data[32]; + int len = ring_buf_peek(&ud->tx_rb, data, sizeof(data)); + if (len == 0) { + return true; + } + + // Write data to UART and remove number of bytes written from buffer. + int ret = uart_fifo_fill(ud->dev, data, len); + if (ret < 0) { + LOG_ERR("failed to fill UART FIFO (err %d)", ret); + return true; + } + ring_buf_get(&ud->tx_rb, data, ret); + return false; +} + +static void serial_rx_work_handler(struct k_work *work) { + struct serial_device *sd = CONTAINER_OF(work, struct serial_device, rx_work); + + // Continue processing data as long as the buffer exceeds the header length + // (13 bytes). + uint8_t data[280]; + while (ring_buf_peek(&sd->rx_rb, data, 13) >= 13) { + // Discard single byte if prefix does not match. + if (memcmp(data, SERIAL_MSG_PREFIX, strlen(SERIAL_MSG_PREFIX))) { + uint8_t discard; + ring_buf_get(&sd->rx_rb, &discard, 1); + continue; + } + + // Stop processing if message body is not completely buffered. + int len = data[12]; + int total = len + 13; + if (ring_buf_size_get(&sd->rx_rb) < total) { + return; + } + + // Check message checksum and handle message. + uint32_t cmd, crc; + ring_buf_get(&sd->rx_rb, data, total); + memcpy(&cmd, &data[4], sizeof(cmd)); + memcpy(&crc, &data[8], sizeof(crc)); + if (crc == crc32_ieee(&data[13], len)) { + serial_handle_rx(cmd, &data[13], len); + } else { + LOG_ERR("received UART message with invalid CRC32 checksum"); + } + } +} + +static void serial_rx_callback(struct serial_device *sd) { + uint8_t c; + while (uart_fifo_read(sd->dev, &c, 1) == 1) { + ring_buf_put(&sd->rx_rb, &c, 1); + } + k_work_submit_to_queue(&serial_wq, &sd->rx_work); +} + +static void serial_callback(const struct device *dev, void *data) { + if (uart_irq_update(dev)) { + struct serial_device *ud = data; + + if (uart_irq_tx_ready(dev)) { + // If transmission complete, disable IRQ until next transmission. + bool complete = serial_tx_callback(ud); + if (complete) { + uart_irq_tx_disable(dev); + } + } + + // TODO TODO TODO lookup index in serial_devs array for slot ID? + if (uart_irq_rx_ready(dev)) { + serial_rx_callback(ud); + } + } +} + +void serial_write(struct serial_device *sd, uint32_t cmd, uint8_t *data, uint8_t len) { + // TODO TODO TODO use buf with size SERIAL_BUF_SIZE. do single + // ring_buf_put() to avoid potential race + uint8_t header[13] = SERIAL_MSG_PREFIX; + memcpy(&header[4], &cmd, sizeof(cmd)); + uint32_t crc = crc32_ieee(data, len); + memcpy(&header[8], &crc, sizeof(crc)); + header[12] = len; + ring_buf_put(&sd->tx_rb, header, sizeof(header)); + ring_buf_put(&sd->tx_rb, data, len); + +#ifdef CONFIG_ZMK_SPLIT_SERIAL_UART_POLL + if (sd->poll) { + k_work_submit_to_queue(&serial_wq, &sd->tx_work); + return; + } +#endif + + uart_irq_tx_enable(sd->dev); +} + +#ifdef CONFIG_ZMK_SPLIT_SERIAL_UART_POLL + +static void serial_tx_work_handler(struct k_work *work) { + struct serial_device *sd = CONTAINER_OF(work, struct serial_device, tx_work); + uint8_t c; + while (ring_buf_get(&sd->tx_rb, &c, sizeof(c))) { + uart_poll_out(sd->dev, c); + } +} + +static void serial_rx_timer_handler(struct k_timer *timer) { + struct serial_device *sd = CONTAINER_OF(timer, struct serial_device, rx_timer); + uint8_t c; + while (uart_poll_in(sd->dev, &c) == 0) { + ring_buf_put(&sd->rx_rb, &c, sizeof(c)); + } + k_work_submit_to_queue(&serial_wq, &sd->rx_work); +} + +#endif + +static int serial_init(void) { + struct k_work_queue_config uart_tx_cfg = {.name = "serial_wq"}; + k_work_queue_start(&serial_wq, serial_wq_stack, K_THREAD_STACK_SIZEOF(serial_wq_stack), 14, + &uart_tx_cfg); + + for (int i = 0; i < CONFIG_ZMK_SPLIT_SERIAL_COUNT; i++) { + struct serial_device *sd = &serial_devs[i]; + if (!device_is_ready(sd->dev)) { + LOG_ERR("failed to get serial device %s", sd->dev->name); + return 1; + } + + // Initialize ring buffer. + ring_buf_init(&sd->rx_rb, sizeof(sd->rx_buf), sd->rx_buf); + ring_buf_init(&sd->tx_rb, sizeof(sd->tx_buf), sd->tx_buf); + + k_work_init(&sd->rx_work, serial_rx_work_handler); +#ifdef CONFIG_ZMK_SPLIT_SERIAL_UART_POLL + if (sd->poll) { + k_timer_init(&sd->rx_timer, serial_rx_timer_handler, NULL); + k_timer_start(&sd->rx_timer, K_NO_WAIT, K_TICKS(1)); + k_work_init(&sd->tx_work, serial_tx_work_handler); + continue; + } +#endif + + int err = uart_irq_callback_user_data_set(sd->dev, serial_callback, sd); + if (err) { + LOG_ERR("failed to set callback for %s (err %d)", sd->dev->name, err); + return err; + } + uart_irq_rx_enable(sd->dev); + } + + return 0; +} + +SYS_INIT(serial_init, APPLICATION, CONFIG_ZMK_SPLIT_INIT_PRIORITY);