diff --git a/app/boards/shields/zmk_uno/zmk_uno_split.dtsi b/app/boards/shields/zmk_uno/zmk_uno_split.dtsi index cd911555b907..261ca81e452b 100644 --- a/app/boards/shields/zmk_uno/zmk_uno_split.dtsi +++ b/app/boards/shields/zmk_uno/zmk_uno_split.dtsi @@ -11,8 +11,13 @@ left_encoder: &encoder { status = "disabled"; }; +&arduino_serial { + status = "okay"; +}; + / { chosen { + zmk,split-uart = &arduino_serial; zmk,physical-layout = &split_matrix_physical_layout; }; diff --git a/app/dts/bindings/zmk,wired-split.yaml b/app/dts/bindings/zmk,wired-split.yaml new file mode 100644 index 000000000000..5895eac2d755 --- /dev/null +++ b/app/dts/bindings/zmk,wired-split.yaml @@ -0,0 +1,20 @@ +# Copyright (c) 2024 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: | + Complete specification of wired split connetion + +compatible: "zmk,wired-split" + +properties: + device: + type: phandle + required: true + description: The UART device for wired split communication + + half-duplex: + type: boolean + description: Enable half-duplex protocol mode. + + dir-gpios: + type: phandle-array diff --git a/app/include/zmk/split/transport/types.h b/app/include/zmk/split/transport/types.h index 35020e835b49..978397820f50 100644 --- a/app/include/zmk/split/transport/types.h +++ b/app/include/zmk/split/transport/types.h @@ -17,6 +17,8 @@ enum zmk_split_transport_peripheral_event_type { }; struct zmk_split_transport_peripheral_event { + enum zmk_split_transport_peripheral_event_type type; + union { struct { uint8_t position; @@ -39,8 +41,6 @@ struct zmk_split_transport_peripheral_event { int32_t value; } input_event; } data; - - enum zmk_split_transport_peripheral_event_type type; } __packed; enum zmk_split_transport_central_command_type { @@ -51,6 +51,8 @@ enum zmk_split_transport_central_command_type { } __packed; struct zmk_split_transport_central_command { + enum zmk_split_transport_central_command_type type; + union { struct { char behavior_dev[16]; @@ -68,6 +70,4 @@ struct zmk_split_transport_central_command { zmk_hid_indicators_t indicators; } set_hid_indicators; } data; - - enum zmk_split_transport_central_command_type type; } __packed; \ No newline at end of file diff --git a/app/src/split/CMakeLists.txt b/app/src/split/CMakeLists.txt index a8edf6834e32..20c730892207 100644 --- a/app/src/split/CMakeLists.txt +++ b/app/src/split/CMakeLists.txt @@ -5,6 +5,10 @@ if (CONFIG_ZMK_SPLIT_BLE) add_subdirectory(bluetooth) endif() +if (CONFIG_ZMK_SPLIT_WIRED) + add_subdirectory(wired) +endif() + if (CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources(app PRIVATE central.c) zephyr_linker_sources(SECTIONS ../../include/linker/zmk-split-transport-central.ld) diff --git a/app/src/split/Kconfig b/app/src/split/Kconfig index a076aa580de5..10d61b99aace 100644 --- a/app/src/split/Kconfig +++ b/app/src/split/Kconfig @@ -9,16 +9,22 @@ if ZMK_SPLIT config ZMK_SPLIT_ROLE_CENTRAL bool "Split central device" -choice ZMK_SPLIT_TRANSPORT - prompt "Split transport" - config ZMK_SPLIT_BLE - bool "BLE" + bool "BLE Split" + default y depends on ZMK_BLE select BT_USER_PHY_UPDATE select BT_AUTO_PHY_UPDATE -endchoice +DT_CHOSEN_ZMK_SPLIT_UART := zmk,split-uart + +config ZMK_SPLIT_WIRED + bool "Wired Split" + default y if !ZMK_SPLIT_BLE + depends on $(dt_chosen_enabled,$(DT_CHOSEN_ZMK_SPLIT_UART)) || DT_HAS_ZMK_WIRED_SPLIT_ENABLED + select SERIAL + select RING_BUFFER + select CRC config ZMK_SPLIT_PERIPHERAL_HID_INDICATORS bool "Peripheral HID Indicators" @@ -29,3 +35,4 @@ config ZMK_SPLIT_PERIPHERAL_HID_INDICATORS endif # ZMK_SPLIT rsource "bluetooth/Kconfig" +rsource "wired/Kconfig" diff --git a/app/src/split/Kconfig.defaults b/app/src/split/Kconfig.defaults index eb23168b9219..f3827b011d8b 100644 --- a/app/src/split/Kconfig.defaults +++ b/app/src/split/Kconfig.defaults @@ -2,3 +2,5 @@ # SPDX-License-Identifier: MIT rsource "bluetooth/Kconfig.defaults" +rsource "wired/Kconfig.defaults" + diff --git a/app/src/split/wired/CMakeLists.txt b/app/src/split/wired/CMakeLists.txt new file mode 100644 index 000000000000..c85ec4029de8 --- /dev/null +++ b/app/src/split/wired/CMakeLists.txt @@ -0,0 +1,3 @@ +target_sources(app PRIVATE wired.c) +target_sources_ifdef(CONFIG_ZMK_SPLIT_ROLE_CENTRAL app PRIVATE central.c) +target_sources_ifndef(CONFIG_ZMK_SPLIT_ROLE_CENTRAL app PRIVATE peripheral.c) \ No newline at end of file diff --git a/app/src/split/wired/Kconfig b/app/src/split/wired/Kconfig new file mode 100644 index 000000000000..ff9b1d84b1e4 --- /dev/null +++ b/app/src/split/wired/Kconfig @@ -0,0 +1,49 @@ +if ZMK_SPLIT_WIRED + +choice ZMK_SPLIT_WIRED_UART_MODE_DEFAULT + prompt "Default UART Mode" + +config ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_ASYNC + bool "Async (DMA) Mode" + # For now, don't use async/DMA on nRF52 due to RX bug (fixed + # in newer Zephyr version?) + depends on SERIAL_SUPPORT_ASYNC && !SOC_FAMILY_NRF + select UART_ASYNC_API + +config ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_INTERRUPT + bool "Interrupt Mode" + depends on SERIAL_SUPPORT_INTERRUPT + select UART_INTERRUPT_DRIVEN + +config ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_POLLING + bool "Polling Mode" + +endchoice + +if ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_POLLING + +config ZMK_SPLIT_WIRED_POLLING_RX_PERIOD + int "Ticks between RX polls" + +endif + +if ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_ASYNC + +config ZMK_SPLIT_WIRED_ASYNC_RX_TIMEOUT + int "RX Timeout (in microseconds) before reporting received data" + +endif + +config ZMK_SPLIT_WIRED_CMD_BUFFER_ITEMS + int "Number of central commands to buffer for TX/RX" + +config ZMK_SPLIT_WIRED_EVENT_BUFFER_ITEMS + int "Number of peripheral events to buffer for TX/RX" + +config ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_TIMEOUT + int "RX timeout (in ms) when polling peripheral(s) and waiting for any response" + +config ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_COMPLETE_TIMEOUT + int "RX complete timeout (in ticks) when polling peripheral(s) after receiving some response data" + +endif \ No newline at end of file diff --git a/app/src/split/wired/Kconfig.defaults b/app/src/split/wired/Kconfig.defaults new file mode 100644 index 000000000000..a2ca78d0c83b --- /dev/null +++ b/app/src/split/wired/Kconfig.defaults @@ -0,0 +1,31 @@ +if ZMK_SPLIT_WIRED + +config ZMK_SPLIT_WIRED_CMD_BUFFER_ITEMS + default 4 + +config ZMK_SPLIT_WIRED_EVENT_BUFFER_ITEMS + default 16 + + +if ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_POLLING + +config ZMK_SPLIT_WIRED_POLLING_RX_PERIOD + default 10 + +endif + + +if ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_ASYNC + +config ZMK_SPLIT_WIRED_ASYNC_RX_TIMEOUT + default 20 + +endif + +config ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_TIMEOUT + default 15 + +config ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_COMPLETE_TIMEOUT + default 20 + +endif \ No newline at end of file diff --git a/app/src/split/wired/central.c b/app/src/split/wired/central.c new file mode 100644 index 000000000000..66250b0963cc --- /dev/null +++ b/app/src/split/wired/central.c @@ -0,0 +1,341 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +#include +#include +#include + +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "wired.h" + +#define DT_DRV_COMPAT zmk_wired_split + +#define IS_HALF_DUPLEX_MODE \ + (DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) && DT_INST_PROP_OR(0, half_duplex, false)) + +#define RX_BUFFER_SIZE \ + ((sizeof(struct event_envelope) + sizeof(struct msg_postfix)) * \ + CONFIG_ZMK_SPLIT_WIRED_EVENT_BUFFER_ITEMS) +#define TX_BUFFER_SIZE \ + ((sizeof(struct command_envelope) + sizeof(struct msg_postfix)) * \ + CONFIG_ZMK_SPLIT_WIRED_CMD_BUFFER_ITEMS) + +#if IS_HALF_DUPLEX_MODE + +static K_SEM_DEFINE(tx_sem, 0, 1); + +#endif + +RING_BUF_DECLARE(rx_buf, RX_BUFFER_SIZE); +RING_BUF_DECLARE(tx_buf, TX_BUFFER_SIZE); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +static const struct device *uart = DEVICE_DT_GET(DT_INST_PHANDLE(0, device)); + +#define HAS_DIR_GPIO (IS_HALF_DUPLEX_MODE && DT_INST_NODE_HAS_PROP(0, dir_gpios)) + +#if HAS_DIR_GPIO + +static const struct gpio_dt_spec dir_gpio = GPIO_DT_SPEC_INST_GET(0, dir_gpios); + +#endif + +#elif DT_HAS_CHOSEN(zmk_split_uart) + +static const struct device *uart = DEVICE_DT_GET(DT_CHOSEN(zmk_split_uart)); + +#define HAS_DIR_GPIO 0 + +#else + +// TODO: Error to link to docs +#error "Need to assign a 'zmk,split-uart` property to an enabled UART" + +#endif + +static void publish_events_work(struct k_work *work); + +K_WORK_DEFINE(publish_events, publish_events_work); + +#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_ASYNC) + +uint8_t async_rx_buf[RX_BUFFER_SIZE / 2][2]; + +static struct zmk_split_wired_async_state async_state = { + .process_tx_work = &publish_events, + .rx_bufs = {async_rx_buf[0], async_rx_buf[1]}, + .rx_bufs_len = RX_BUFFER_SIZE / 2, + .rx_size_process_trigger = MSG_EXTRA_SIZE + 1, + .rx_buf = &rx_buf, + .tx_buf = &tx_buf, +#if HAS_DIR_GPIO + .dir_gpio = &dir_gpio, +#endif + ; + +#endif + +#if IS_HALF_DUPLEX_MODE + +static int can_tx(void) { return k_sem_take(&tx_sem, K_NO_WAIT); } + +#else + +static inline int can_tx(void) { return 0; } + +#endif + +static void begin_tx(void) { +#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_INTERRUPT) + uart_irq_tx_enable(uart); +#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_ASYNC) + zmk_split_wired_async_tx(&async_state); +#else + k_work_submit(&wired_central_tx_work); +#endif +} + +static ssize_t get_payload_data_size(const struct zmk_split_transport_central_command *cmd) { + switch (cmd->type) { + case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_POLL_EVENTS: + return 0; + case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_TRIGGER_BEHAVIOR: + return sizeof(cmd->data.trigger_behavior); + case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_PHYSICAL_LAYOUT: + return sizeof(cmd->data.set_physical_layout); + case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_HID_INDICATORS: + return sizeof(cmd->data.set_hid_indicators); + default: + return -ENOTSUP; + } +} + +static int split_central_wired_send_command(uint8_t source, + struct zmk_split_transport_central_command cmd) { + if (source != 0) { + return -EINVAL; + } + + ssize_t data_size = get_payload_data_size(&cmd); + if (data_size < 0) { + LOG_WRN("Failed to determine payload data size %d", data_size); + return data_size; + } + + // Data + type + source + size_t payload_size = + data_size + sizeof(source) + sizeof(enum zmk_split_transport_central_command_type); + + if (ring_buf_space_get(&tx_buf) < MSG_EXTRA_SIZE + payload_size) { + LOG_WRN("No room to send command to the peripheral %d", source); + return -ENOSPC; + } + + struct command_envelope env = {.prefix = + { + .magic_prefix = ZMK_SPLIT_WIRED_ENVELOPE_MAGIC_PREFIX, + .payload_size = payload_size, + }, + .payload = { + .source = source, + .cmd = cmd, + }}; + + struct msg_postfix postfix = {.crc = + crc32_ieee((void *)&env, sizeof(env.prefix) + payload_size)}; + + ring_buf_put(&tx_buf, (uint8_t *)&env, sizeof(env.prefix) + payload_size); + ring_buf_put(&tx_buf, (uint8_t *)&postfix, sizeof(postfix)); + + if (can_tx() >= 0) { + begin_tx(); + } + + return 0; +} + +#if IS_HALF_DUPLEX_MODE + +void rx_done_cb(struct k_work *work); + +static K_WORK_DELAYABLE_DEFINE(rx_done_work, rx_done_cb); + +void rx_done_cb(struct k_work *work) { + k_sem_give(&tx_sem); + + // Poll for the next event data! + split_central_wired_send_command(0, + (struct zmk_split_transport_central_command){ + .type = ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_POLL_EVENTS, + }); + + begin_tx(); + + k_work_reschedule(&rx_done_work, K_MSEC(CONFIG_ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_TIMEOUT)); +} + +#endif + +#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_INTERRUPT) + +static void serial_cb(const struct device *dev, void *user_data) { + + while (uart_irq_update(dev) && uart_irq_is_pending(dev)) { + if (uart_irq_rx_ready(dev)) { + zmk_split_wired_fifo_read(dev, &rx_buf, &publish_events, NULL); +#if IS_HALF_DUPLEX_MODE + k_work_reschedule(&rx_done_work, + K_TICKS(CONFIG_ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_COMPLETE_TIMEOUT)); +#endif + } + + if (uart_irq_tx_complete(dev)) { + + if (ring_buf_size_get(&tx_buf) == 0) { + uart_irq_tx_disable(dev); +#if HAS_DIR_GPIO + gpio_pin_set_dt(&dir_gpio, 0); +#endif + } + } + + if (uart_irq_tx_ready(dev)) { +#if HAS_DIR_GPIO + gpio_pin_set_dt(&dir_gpio, 1); +#endif + zmk_split_wired_fifo_fill(dev, &tx_buf); + } + } +} + +#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_POLLING) + +static void send_pending_tx_work_cb(struct k_work *work) { + zmk_split_wired_poll_out(&tx_buf, uart); +} + +static void read_timer_cb(struct k_timer *_timer) { + zmk_split_wired_poll_in(&rx_buf, uart, &publish_events, sizeof(struct event_envelope)); + // Check if we found any bytes, read some, or read all the bytes in the RX +} + +static K_WORK_DEFINE(wired_central_tx_work, send_pending_tx_work_cb); +static K_TIMER_DEFINE(wired_central_read_timer, read_timer_cb, NULL); + +#endif + +static int zmk_split_wired_central_init(void) { + if (!device_is_ready(uart)) { + return -ENODEV; + } + +#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_INTERRUPT) + + int ret = uart_irq_callback_user_data_set(uart, serial_cb, NULL); + + if (ret < 0) { + if (ret == -ENOTSUP) { + LOG_ERR("Interrupt-driven UART API support not enabled"); + } else if (ret == -ENOSYS) { + LOG_ERR("UART device does not support interrupt-driven API"); + } else { + LOG_ERR("Error setting UART callback: %d\n", ret); + } + return ret; + } + + uart_irq_rx_enable(uart); + +#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_ASYNC) + + async_state.uart = uart; + int ret = zmk_split_wired_async_init(&async_state); + if (ret < 0) { + LOG_ERR("Failed to set up async wired split UART (%d)", ret); + return ret; + } + +#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_*) + +#if IS_HALF_DUPLEX_MODE + +#if HAS_DIR_GPIO + LOG_DBG("CONFIGURING AS OUTPUT"); + gpio_pin_configure_dt(&dir_gpio, GPIO_OUTPUT_INACTIVE); +#endif + + k_work_schedule(&rx_done_work, K_MSEC(CONFIG_ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_TIMEOUT)); + +#endif + +#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_POLLING) + k_timer_start(&wired_central_read_timer, K_TICKS(CONFIG_ZMK_SPLIT_WIRED_POLLING_RX_PERIOD), + K_TICKS(CONFIG_ZMK_SPLIT_WIRED_POLLING_RX_PERIOD)); +#endif + + return 0; +} + +SYS_INIT(zmk_split_wired_central_init, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT); + +static int split_central_bt_get_available_source_ids(uint8_t *sources) { + sources[0] = 0; + + return 1; +} + +static const struct zmk_split_transport_central_api central_api = { + .send_command = split_central_wired_send_command, + .get_available_source_ids = split_central_bt_get_available_source_ids, +}; + +ZMK_SPLIT_TRANSPORT_CENTRAL_REGISTER(wired_central, ¢ral_api); + +static void publish_events_work(struct k_work *work) { + +#if IS_HALF_DUPLEX_MODE + k_work_reschedule(&rx_done_work, + K_MSEC(CONFIG_ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_COMPLETE_TIMEOUT)); +#endif // IS_HALF_DUPLEX_MODE + + while (ring_buf_size_get(&rx_buf) > MSG_EXTRA_SIZE) { + struct event_envelope env; + int item_err = + zmk_split_wired_get_item(&rx_buf, (uint8_t *)&env, sizeof(struct event_envelope)); + switch (item_err) { + case 0: + zmk_split_transport_central_peripheral_event_handler(&wired_central, env.payload.source, + env.payload.event); + break; + case -EAGAIN: + return; + default: + LOG_WRN("Issue fetching an item from the RX buffer: %d", item_err); + return; + } + } +} diff --git a/app/src/split/wired/peripheral.c b/app/src/split/wired/peripheral.c new file mode 100644 index 000000000000..63aef1f6aaa1 --- /dev/null +++ b/app/src/split/wired/peripheral.c @@ -0,0 +1,307 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +#include +#include +#include + +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "wired.h" + +#define DT_DRV_COMPAT zmk_wired_split + +#define IS_HALF_DUPLEX_MODE \ + (DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) && DT_INST_PROP_OR(0, half_duplex, false)) + +#define TX_BUFFER_SIZE \ + ((sizeof(struct event_envelope) + sizeof(struct msg_postfix)) * \ + CONFIG_ZMK_SPLIT_WIRED_EVENT_BUFFER_ITEMS) +#define RX_BUFFER_SIZE \ + ((sizeof(struct command_envelope) + sizeof(struct msg_postfix)) * \ + CONFIG_ZMK_SPLIT_WIRED_CMD_BUFFER_ITEMS) + +RING_BUF_DECLARE(chosen_rx_buf, RX_BUFFER_SIZE); +RING_BUF_DECLARE(chosen_tx_buf, TX_BUFFER_SIZE); + +static const uint8_t peripheral_id = 0; + +K_SEM_DEFINE(tx_sem, 0, 1); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +#define HAS_DIR_GPIO (DT_INST_NODE_HAS_PROP(0, dir_gpios)) +static const struct device *uart = DEVICE_DT_GET(DT_INST_PHANDLE(0, device)); + +#if HAS_DIR_GPIO + +static const struct gpio_dt_spec dir_gpio = GPIO_DT_SPEC_INST_GET(0, dir_gpios); + +#endif + +#elif DT_HAS_CHOSEN(zmk_split_uart) + +#define HAS_DIR_GPIO 0 + +static const struct device *uart = DEVICE_DT_GET(DT_CHOSEN(zmk_split_uart)); + +#else + +// TODO: Error to link to docs +#error "Need to assign a 'zmk,split-uart` node or create a wired split node with details" + +#endif + +static void publish_commands_work(struct k_work *work); + +K_WORK_DEFINE(publish_commands, publish_commands_work); + +static void process_tx_cb(void); +K_MSGQ_DEFINE(cmd_msg_queue, sizeof(struct zmk_split_transport_central_command), 3, 4); + +#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_ASYNC) + +uint8_t async_rx_buf[RX_BUFFER_SIZE / 2][2]; + +static struct zmk_split_wired_async_state async_state = { + .rx_bufs = {async_rx_buf[0], async_rx_buf[1]}, + .rx_bufs_len = RX_BUFFER_SIZE / 2, + .rx_size_process_trigger = sizeof(struct command_envelope), + .process_tx_callback = process_tx_cb, + .rx_buf = &chosen_rx_buf, + .tx_buf = &chosen_tx_buf, +#if HAS_DIR_GPIO + .dir_gpio = &dir_gpio, +#endif +}; + +#endif +#if HAS_DIR_GPIO + +static void set_dir(uint8_t tx) { gpio_pin_set_dt(&dir_gpio, tx); } + +#else + +static inline void set_dir(uint8_t tx) {} + +#endif + +#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_INTERRUPT) + +static void serial_cb(const struct device *dev, void *user_data) { + while (uart_irq_update(dev) && uart_irq_is_pending(dev)) { + if (uart_irq_rx_ready(dev)) { + zmk_split_wired_fifo_read(dev, &chosen_rx_buf, NULL, process_tx_cb); + } + + if (uart_irq_tx_complete(dev)) { + if (ring_buf_size_get(&chosen_tx_buf) == 0) { + uart_irq_tx_disable(dev); + } + + set_dir(0); + } + + if (uart_irq_tx_ready(dev)) { + set_dir(1); + zmk_split_wired_fifo_fill(dev, &chosen_tx_buf); + } + } +} + +#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_POLLING) + +static void send_pending_tx_work_cb(struct k_work *work) { + zmk_split_wired_poll_out(&chosen_tx_buf, uart); +} + +static K_WORK_DEFINE(send_pending_tx, send_pending_tx_work_cb); + +static void wired_peripheral_read_tick_cb(struct k_timer *timer) { + zmk_split_wired_poll_in(&chosen_rx_buf, uart, &publish_commands, + sizeof(struct command_envelope)); +} + +static K_TIMER_DEFINE(wired_peripheral_read_timer, wired_peripheral_read_tick_cb, NULL); + +#endif + +static int zmk_split_wired_peripheral_init(void) { + if (!device_is_ready(uart)) { + return -ENODEV; + } + +#if HAS_DIR_GPIO + gpio_pin_configure_dt(&dir_gpio, GPIO_OUTPUT_INACTIVE); +#endif + +#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_INTERRUPT) + /* configure interrupt and callback to receive data */ + int ret = uart_irq_callback_user_data_set(uart, serial_cb, NULL); + + if (ret < 0) { + if (ret == -ENOTSUP) { + LOG_ERR("Interrupt-driven UART API support not enabled"); + } else if (ret == -ENOSYS) { + LOG_ERR("UART device does not support interrupt-driven API"); + } else { + LOG_ERR("Error setting UART callback: %d\n", ret); + } + return ret; + } + + uart_irq_rx_enable(uart); +#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_ASYNC) + async_state.uart = uart; + int ret = zmk_split_wired_async_init(&async_state); + if (ret < 0) { + LOG_ERR("Failed to set up async wired split UART (%d)", ret); + return ret; + } + +#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_POLLING) + k_timer_start(&wired_peripheral_read_timer, K_TICKS(CONFIG_ZMK_SPLIT_WIRED_POLLING_RX_PERIOD), + K_TICKS(CONFIG_ZMK_SPLIT_WIRED_POLLING_RX_PERIOD)); +#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_INTERRUPT) + + return 0; +} + +SYS_INIT(zmk_split_wired_peripheral_init, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT); + +static void begin_tx(void) { +#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_INTERRUPT) + uart_irq_tx_enable(uart); +#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_ASYNC) + zmk_split_wired_async_tx(&async_state); +#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_POLLING) + k_work_submit(&send_pending_tx); +#endif +} + +static ssize_t get_payload_data_size(const struct zmk_split_transport_peripheral_event *evt) { + switch (evt->type) { + case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_INPUT_EVENT: + return sizeof(evt->data.input_event); + case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_KEY_POSITION: + return sizeof(evt->data.key_position); + case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_SENSOR_EVENT: + return sizeof(evt->data.sensor_event); + default: + return -ENOTSUP; + } +} + +static int +split_peripheral_wired_report_event(const struct zmk_split_transport_peripheral_event *event) { + ssize_t data_size = get_payload_data_size(event); + if (data_size < 0) { + LOG_WRN("Failed to determine payload data size %d", data_size); + return data_size; + } + + // Data + type + source + size_t payload_size = + data_size + sizeof(peripheral_id) + sizeof(enum zmk_split_transport_peripheral_event_type); + + if (ring_buf_space_get(&chosen_tx_buf) < MSG_EXTRA_SIZE + payload_size) { + LOG_WRN("No room to send peripheral to the central (have %d but only space for %d)", + MSG_EXTRA_SIZE + payload_size, ring_buf_space_get(&chosen_tx_buf)); + return -ENOSPC; + } + + struct event_envelope env = {.prefix = + { + .magic_prefix = ZMK_SPLIT_WIRED_ENVELOPE_MAGIC_PREFIX, + .payload_size = payload_size, + }, + .payload = { + .source = peripheral_id, + .event = *event, + }}; + + struct msg_postfix postfix = {.crc = + crc32_ieee((void *)&env, sizeof(env.prefix) + payload_size)}; + + LOG_HEXDUMP_DBG(&env, sizeof(env.prefix) + payload_size, "Payload"); + + size_t put = ring_buf_put(&chosen_tx_buf, (uint8_t *)&env, sizeof(env.prefix) + payload_size); + if (put != sizeof(env.prefix) + payload_size) { + LOG_WRN("Failed to put the whole message (%d vs %d)", put, + sizeof(env.prefix) + payload_size); + } + put = ring_buf_put(&chosen_tx_buf, (uint8_t *)&postfix, sizeof(postfix)); + if (put != sizeof(postfix)) { + LOG_WRN("Failed to put the whole message (%d vs %d)", put, sizeof(postfix)); + } + +#if !IS_HALF_DUPLEX_MODE + begin_tx(); +#endif + + return 0; +} + +static const struct zmk_split_transport_peripheral_api peripheral_api = { + .report_event = split_peripheral_wired_report_event, +}; + +ZMK_SPLIT_TRANSPORT_PERIPHERAL_REGISTER(wired_peripheral, &peripheral_api); + +static void process_tx_cb(void) { + while (ring_buf_size_get(&chosen_rx_buf) > MSG_EXTRA_SIZE) { + struct command_envelope env; + int item_err = zmk_split_wired_get_item(&chosen_rx_buf, (uint8_t *)&env, + sizeof(struct command_envelope)); + switch (item_err) { + case 0: + if (env.payload.cmd.type == ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_POLL_EVENTS) { + begin_tx(); + } else { + int ret = k_msgq_put(&cmd_msg_queue, &env.payload.cmd, K_NO_WAIT); + if (ret < 0) { + LOG_WRN("Failed to queue command for processing (%d)", ret); + return; + } + + k_work_submit(&publish_commands); + } + break; + case -EAGAIN: + return; + default: + LOG_WRN("Issue fetching an item from the RX buffer: %d", item_err); + return; + } + } +} +static void publish_commands_work(struct k_work *work) { + struct zmk_split_transport_central_command cmd; + + while (k_msgq_get(&cmd_msg_queue, &cmd, K_NO_WAIT) >= 0) { + zmk_split_transport_peripheral_command_handler(&wired_peripheral, cmd); + } +} diff --git a/app/src/split/wired/wired.c b/app/src/split/wired/wired.c new file mode 100644 index 000000000000..4130b3fe6075 --- /dev/null +++ b/app/src/split/wired/wired.c @@ -0,0 +1,322 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include "wired.h" + +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_POLLING) + +void zmk_split_wired_poll_out(struct ring_buf *tx_buf, const struct device *uart) { + uint8_t *buf; + uint32_t claim_len; + while ((claim_len = ring_buf_get_claim(tx_buf, &buf, MIN(32, tx_buf->size))) > 0) { + LOG_HEXDUMP_DBG(buf, claim_len, "TX Bytes"); + for (int i = 0; i < claim_len; i++) { + uart_poll_out(uart, buf[i]); + } + + ring_buf_get_finish(tx_buf, claim_len); + } +} + +int zmk_split_wired_poll_in(struct ring_buf *rx_buf, const struct device *uart, + struct k_work *process_data_work, + zmk_split_wired_process_tx_callback_t process_data_cb, + size_t envelope_size) { + uint8_t *buf; + uint32_t read = 0; + uint32_t claim_len = ring_buf_put_claim(rx_buf, &buf, ring_buf_space_get(rx_buf)); + if (claim_len < 1) { + LOG_WRN("No room available for reading in from the serial port"); + return -ENOSPC; + } + + bool all_read = false; + while (read < claim_len) { + if (uart_poll_in(uart, buf + read) < 0) { + all_read = true; + break; + } + + read++; + } + + ring_buf_put_finish(rx_buf, read); + + if (ring_buf_size_get(rx_buf) >= envelope_size) { + if (process_data_work) { + k_work_submit(process_data_work); + } else if (process_data_cb) { + process_data_cb(); + } + } + + // TODO: Also indicate if no bytes read at all? + return (all_read ? 1 : 0); +} + +#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_POLLING) + +#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_INTERRUPT) + +void zmk_split_wired_fifo_read(const struct device *dev, struct ring_buf *buf, + struct k_work *process_work, + zmk_split_wired_process_tx_callback_t process_cb) { + // TODO: Add error checking on platforms that support it + uint32_t last_read = 0, len = 0; + do { + uint8_t *buffer; + len = ring_buf_put_claim(buf, &buffer, buf->size); + if (len > 0) { + last_read = uart_fifo_read(dev, buffer, len); + + ring_buf_put_finish(buf, last_read); + } else { + LOG_ERR("Dropping incoming RPC byte, insufficient room in the RX buffer. Bump " + "CONFIG_ZMK_STUDIO_RPC_RX_BUF_SIZE."); + uint8_t dummy; + last_read = uart_fifo_read(dev, &dummy, 1); + } + } while (last_read && last_read == len); + + if (process_work) { + k_work_submit(process_work); + } else if (process_cb) { + process_cb(); + } +} + +void zmk_split_wired_fifo_fill(const struct device *dev, struct ring_buf *tx_buf) { + uint32_t len; + while ((len = ring_buf_size_get(tx_buf)) > 0) { + uint8_t *buf; + uint32_t claim_len = ring_buf_get_claim(tx_buf, &buf, tx_buf->size); + + if (claim_len <= 0) { + break; + } + + int sent = uart_fifo_fill(dev, buf, claim_len); + + ring_buf_get_finish(tx_buf, MAX(sent, 0)); + + if (sent <= 0) { + break; + } + } + + // if (ring_buf_size_get(tx_buf) == 0) { + // uart_irq_tx_disable(dev); + // } +} + +#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_INTERRUPT) + +#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_ASYNC) + +enum ASYNC_STATE_BITS { + ASYNC_STATE_BIT_RXBUF0_USED = 0, + ASYNC_STATE_BIT_RXBUF1_USED, +}; + +void zmk_split_wired_async_tx(struct zmk_split_wired_async_state *state) { + uint8_t *buf; + uint32_t claim_len = ring_buf_get_claim(state->tx_buf, &buf, ring_buf_size_get(state->tx_buf)); + + if (claim_len <= 0) { + return; + } + + if (state->dir_gpio) { + gpio_pin_set_dt(state->dir_gpio, 1); + } + +#if !IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) + LOG_DBG("Sending %d", claim_len); +#endif + int err = uart_tx(state->uart, buf, claim_len, SYS_FOREVER_US); + if (err < 0) { + LOG_DBG("NO TX %d", err); + } +} + +static void restart_rx_work_cb(struct k_work *work) { + struct k_work_delayable *dwork = k_work_delayable_from_work(work); + struct zmk_split_wired_async_state *state = + CONTAINER_OF(dwork, struct zmk_split_wired_async_state, restart_rx_work); + + atomic_set_bit(&state->state, ASYNC_STATE_BIT_RXBUF0_USED); + atomic_clear_bit(&state->state, ASYNC_STATE_BIT_RXBUF1_USED); + + LOG_WRN("RESTART!"); + + int ret = uart_rx_enable(state->uart, state->rx_bufs[0], state->rx_bufs_len, + CONFIG_ZMK_SPLIT_WIRED_ASYNC_RX_TIMEOUT); + if (ret < 0) { + LOG_ERR("Failed to enable RX (%d)", ret); + } +} + +static void async_uart_cb(const struct device *dev, struct uart_event *ev, void *user_data) { + struct zmk_split_wired_async_state *state = (struct zmk_split_wired_async_state *)user_data; + + switch (ev->type) { + case UART_TX_ABORTED: + // This can only really occur for a TX timeout for a HW flow control UART setup. What to do + // here in practice? + LOG_WRN("TX Aborted"); + break; + case UART_TX_DONE: + LOG_DBG("TX Done %d", ev->data.tx.len); + ring_buf_get_finish(state->tx_buf, ev->data.tx.len); + if (ring_buf_size_get(state->tx_buf) > 0) { + zmk_split_wired_async_tx(state); + } else { + if (state->dir_gpio) { + gpio_pin_set_dt(state->dir_gpio, 0); + } + } + break; + case UART_RX_RDY: { + size_t received = + ring_buf_put(state->rx_buf, &ev->data.rx.buf[ev->data.rx.offset], ev->data.rx.len); + if (received < ev->data.rx.len) { + LOG_ERR("RX overrun!"); + break; + } + + LOG_DBG("RX %d and now buffer is %d", received, ring_buf_size_get(state->rx_buf)); + if (state->process_tx_callback) { + state->process_tx_callback(); + } else if (state->process_tx_work) { + k_work_submit(state->process_tx_work); + } + + // if (ring_buf_size_get(state->rx_buf) >= state->rx_size_process_trigger) { + // LOG_DBG("RX %d and now buffer is %d", received, ring_buf_size_get(state->rx_buf)); + // } + break; + } + case UART_RX_BUF_RELEASED: + if (ev->data.rx_buf.buf == state->rx_bufs[0]) { + atomic_clear_bit(&state->state, ASYNC_STATE_BIT_RXBUF0_USED); + } else if (ev->data.rx_buf.buf == state->rx_bufs[1]) { + atomic_clear_bit(&state->state, ASYNC_STATE_BIT_RXBUF1_USED); + } + + break; + case UART_RX_BUF_REQUEST: + if (!atomic_test_and_set_bit(&state->state, ASYNC_STATE_BIT_RXBUF0_USED)) { + uart_rx_buf_rsp(state->uart, state->rx_bufs[0], state->rx_bufs_len); + break; + } + + if (!atomic_test_and_set_bit(&state->state, ASYNC_STATE_BIT_RXBUF1_USED)) { + uart_rx_buf_rsp(state->uart, state->rx_bufs[1], state->rx_bufs_len); + break; + } + + LOG_WRN("No RX buffers available!"); + break; + case UART_RX_STOPPED: + // LOG_WRN("UART RX Stopped %d with state %ld", ev->data.rx_stop.reason, state->state); + break; + case UART_RX_DISABLED: { + k_work_schedule(&state->restart_rx_work, K_MSEC(1)); + + break; + } + default: + break; + } +} + +int zmk_split_wired_async_init(struct zmk_split_wired_async_state *state) { + __ASSERT(state != NULL, "State is null"); + + k_work_init_delayable(&state->restart_rx_work, restart_rx_work_cb); + + int ret = uart_callback_set(state->uart, async_uart_cb, state); + if (ret < 0) { + LOG_ERR("Failed to set up async callback on UART (%d)", ret); + return ret; + } + + atomic_set_bit(&state->state, ASYNC_STATE_BIT_RXBUF0_USED); + + ret = uart_rx_enable(state->uart, state->rx_bufs[0], state->rx_bufs_len, + CONFIG_ZMK_SPLIT_WIRED_ASYNC_RX_TIMEOUT); + if (ret < 0) { + LOG_ERR("Failed to enable RX (%d)", ret); + return ret; + } + + return 0; +} + +#endif + +int zmk_split_wired_get_item(struct ring_buf *rx_buf, uint8_t *env, size_t env_size) { + while (ring_buf_size_get(rx_buf) > sizeof(struct msg_prefix) + sizeof(struct msg_postfix)) { + struct msg_prefix prefix; + + __ASSERT_EVAL( + (void)ring_buf_peek(rx_buf, (uint8_t *)&prefix, sizeof(prefix)), + uint32_t peek_read = ring_buf_peek(rx_buf, (uint8_t *)&prefix, sizeof(prefix)), + peek_read == sizeof(prefix), "Somehow read less than we expect from the RX buffer"); + + if (memcmp(&prefix.magic_prefix, &ZMK_SPLIT_WIRED_ENVELOPE_MAGIC_PREFIX, + sizeof(prefix.magic_prefix)) != 0) { + uint8_t discarded_byte; + ring_buf_get(rx_buf, &discarded_byte, 1); + + LOG_WRN("Prefix mismatch, discarding byte %0x", discarded_byte); + + continue; + } + + size_t payload_to_read = sizeof(prefix) + prefix.payload_size; + + if (payload_to_read > env_size) { + LOG_WRN("Invalid message with payload %d bigger than expected max %d", payload_to_read, + env_size); + return -EINVAL; + } + + if (ring_buf_size_get(rx_buf) < payload_to_read + sizeof(struct msg_postfix)) { + return -EAGAIN; + } + + // Now that prefix matches, read it out so we can read the rest of the payload. + __ASSERT_EVAL((void)ring_buf_get(rx_buf, env, payload_to_read), + uint32_t read = ring_buf_get(rx_buf, env, payload_to_read), + read == payload_to_read, + "Somehow read less than we expect from the RX buffer"); + + struct msg_postfix postfix; + __ASSERT_EVAL((void)ring_buf_get(rx_buf, (uint8_t *)&postfix, sizeof(postfix)), + uint32_t read = ring_buf_get(rx_buf, (uint8_t *)&postfix, sizeof(postfix)), + read == sizeof(postfix), + "Somehow read less of the postfix than we expect from the RX buffer"); + + uint32_t crc = crc32_ieee(env, payload_to_read); + if (crc != postfix.crc) { + LOG_WRN("Data corruption in received peripheral event, ignoring %d vs %d", crc, + postfix.crc); + return -EINVAL; + } + + return 0; + } + + return -EAGAIN; +} \ No newline at end of file diff --git a/app/src/split/wired/wired.h b/app/src/split/wired/wired.h new file mode 100644 index 000000000000..f235c8723118 --- /dev/null +++ b/app/src/split/wired/wired.h @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include + +#include + +#define ZMK_SPLIT_WIRED_ENVELOPE_MAGIC_PREFIX "ZmKw" + +struct msg_prefix { + uint8_t magic_prefix[sizeof(ZMK_SPLIT_WIRED_ENVELOPE_MAGIC_PREFIX) - 1]; + uint8_t payload_size; +} __packed; + +struct command_payload { + uint8_t source; + struct zmk_split_transport_central_command cmd; +} __packed; + +struct command_envelope { + struct msg_prefix prefix; + struct command_payload payload; +} __packed; + +struct event_payload { + uint8_t source; + struct zmk_split_transport_peripheral_event event; +} __packed; + +struct event_envelope { + struct msg_prefix prefix; + struct event_payload payload; +} __packed; + +struct msg_postfix { + uint32_t crc; +} __packed; + +#define MSG_EXTRA_SIZE (sizeof(struct msg_prefix) + sizeof(struct msg_postfix)) + +typedef void (*zmk_split_wired_process_tx_callback_t)(void); + +#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_POLLING) + +void zmk_split_wired_poll_out(struct ring_buf *tx_buf, const struct device *uart); + +int zmk_split_wired_poll_in(struct ring_buf *rx_buf, const struct device *uart, + struct k_work *process_data_work, + zmk_split_wired_process_tx_callback_t process_data_cb, + size_t envelope_size); + +#endif + +#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_INTERRUPT) + +void zmk_split_wired_fifo_read(const struct device *dev, struct ring_buf *buf, + struct k_work *process_work, + zmk_split_wired_process_tx_callback_t process_cb); +void zmk_split_wired_fifo_fill(const struct device *dev, struct ring_buf *tx_buf); + +#endif + +#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_ASYNC) + +struct zmk_split_wired_async_state { + atomic_t state; + + uint8_t *rx_bufs[2]; + size_t rx_bufs_len; + size_t rx_size_process_trigger; + + struct ring_buf *tx_buf; + struct ring_buf *rx_buf; + + zmk_split_wired_process_tx_callback_t process_tx_callback; + + const struct device *uart; + + struct k_work_delayable restart_rx_work; + struct k_work *process_tx_work; + const struct gpio_dt_spec *dir_gpio; +}; + +void zmk_split_wired_async_tx(struct zmk_split_wired_async_state *state); +int zmk_split_wired_async_init(struct zmk_split_wired_async_state *state); + +#endif + +int zmk_split_wired_get_item(struct ring_buf *rx_buf, uint8_t *env, size_t env_size); \ No newline at end of file