Skip to content

Commit

Permalink
feat(split): Add full-duplex wired split support
Browse files Browse the repository at this point in the history
* Depends on full-duplex hardware UART for communication.
* Supports all existing central commands/peripheral events, including
  sensors/inputs from peripherals.
* Only one wired split peripheral supported (for now)
* Relies on chosen `zmk,split-uart` referencing the UART device.
  • Loading branch information
petejohanson committed Jan 17, 2025
1 parent cd1eb98 commit 47b7001
Show file tree
Hide file tree
Showing 11 changed files with 682 additions and 5 deletions.
5 changes: 5 additions & 0 deletions app/boards/shields/zmk_uno/zmk_uno_split.dtsi
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
4 changes: 4 additions & 0 deletions app/src/split/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 11 additions & 5 deletions app/src/split/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,21 @@ 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 $(dt_chosen_enabled,$(DT_CHOSEN_ZMK_SPLIT_UART)) if !ZMK_SPLIT_BLE
select SERIAL
select RING_BUFFER
select CRC

config ZMK_SPLIT_PERIPHERAL_HID_INDICATORS
bool "Peripheral HID Indicators"
Expand All @@ -29,3 +34,4 @@ config ZMK_SPLIT_PERIPHERAL_HID_INDICATORS
endif # ZMK_SPLIT

rsource "bluetooth/Kconfig"
rsource "wired/Kconfig"
2 changes: 2 additions & 0 deletions app/src/split/Kconfig.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
# SPDX-License-Identifier: MIT

rsource "bluetooth/Kconfig.defaults"
rsource "wired/Kconfig.defaults"

3 changes: 3 additions & 0 deletions app/src/split/wired/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
29 changes: 29 additions & 0 deletions app/src/split/wired/Kconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
if ZMK_SPLIT_WIRED

choice ZMK_SPLIT_WIRED_UART_MODE_DEFAULT
prompt "Default UART Mode"
default ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_INTERRUPT if SERIAL_SUPPORT_INTERRUPT

config ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_POLLING
bool "Polling Mode"

config ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_INTERRUPT
bool "Interrupt Mode"
select UART_INTERRUPT_DRIVEN

endchoice

if ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_POLLING

config ZMK_SPLIT_WIRED_POLLING_RX_PERIOD
int "Ticks between RX polls"

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"

endif
17 changes: 17 additions & 0 deletions app/src/split/wired/Kconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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

endif
227 changes: 227 additions & 0 deletions app/src/split/wired/central.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/

#include <zephyr/types.h>
#include <zephyr/init.h>

#include <zephyr/settings/settings.h>
#include <zephyr/sys/crc.h>
#include <zephyr/sys/ring_buffer.h>

#include <zephyr/logging/log.h>

LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);

#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/uart.h>

#include <zmk/stdlib.h>
#include <zmk/behavior.h>
#include <zmk/sensors.h>
#include <zmk/split/transport/central.h>
#include <zmk/event_manager.h>
#include <zmk/events/position_state_changed.h>
#include <zmk/events/sensor_event.h>
#include <zmk/pointing/input_split.h>
#include <zmk/hid_indicators_types.h>
#include <zmk/physical_layouts.h>

#include "wired.h"

#define RX_BUFFER_SIZE (sizeof(struct event_envelope) * CONFIG_ZMK_SPLIT_WIRED_EVENT_BUFFER_ITEMS)
#define TX_BUFFER_SIZE (sizeof(struct command_envelope) * CONFIG_ZMK_SPLIT_WIRED_CMD_BUFFER_ITEMS)

struct wired_bus_state {
struct k_work event_publish_work;

struct ring_buf rx_buf;
uint8_t rx_buffer[RX_BUFFER_SIZE];

struct ring_buf tx_buf;
uint8_t tx_buffer[TX_BUFFER_SIZE];
};

struct wired_bus {
const struct device *uart;
struct gpio_dt_spec recv_gpio;
struct wired_bus_state *state;
};

struct wired_peripheral {
uint8_t reg;
const struct wired_bus *bus;
};

#if DT_HAS_CHOSEN(zmk_split_uart)

struct wired_bus_state bus_state = {};

static const struct wired_bus buses[] = {{
.uart = DEVICE_DT_GET(DT_CHOSEN(zmk_split_uart)),
.state = &bus_state,
}};

static const struct wired_peripheral peripherals[] = {{
.bus = &buses[0],
.reg = 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);

#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_INTERRUPT)

static void serial_cb(const struct device *dev, void *user_data) {
const struct wired_bus *bus = (const struct wired_bus *)user_data;

while (uart_irq_update(dev) && uart_irq_is_pending(dev)) {
if (uart_irq_rx_ready(dev)) {
zmk_split_wired_fifo_read(dev, &bus->state->rx_buf, &bus->state->event_publish_work);
}

if (uart_irq_tx_ready(dev)) {
zmk_split_wired_fifo_fill(dev, &bus->state->tx_buf);
}
}
}

#else

static void send_pending_tx_work_cb(struct k_work *work) {
for (size_t i = 0; i < ARRAY_SIZE(buses); i++) {
zmk_split_wired_poll_out(&buses[i].state->tx_buf, buses[i].uart);
}
}

static void read_timer_cb(struct k_timer *_timer) {
for (size_t i = 0; i < ARRAY_SIZE(buses); i++) {
zmk_split_wired_poll_in(&buses[i].state->rx_buf, buses[i].uart,
&buses[i].state->event_publish_work, sizeof(struct event_envelope));
}
}

static K_WORK_DEFINE(wired_central_rx_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) {
LOG_DBG("");
for (size_t i = 0; i < ARRAY_SIZE(buses); i++) {
if (!device_is_ready(buses[i].uart)) {
return -ENODEV;
}

k_work_init(&buses[i].state->event_publish_work, publish_events_work);
ring_buf_init(&buses[i].state->rx_buf, RX_BUFFER_SIZE, buses[i].state->rx_buffer);
ring_buf_init(&buses[i].state->tx_buf, TX_BUFFER_SIZE, buses[i].state->tx_buffer);

#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_INTERRUPT)
int ret = uart_irq_callback_user_data_set(buses[i].uart, serial_cb, (void *)&buses[i]);

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(buses[i].uart);
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_INTERRUPT)
}

#if !IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_INTERRUPT)
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_send_command(uint8_t source,
struct zmk_split_transport_central_command cmd) {

if (source >= ARRAY_SIZE(peripherals)) {
return -EINVAL;
}

const struct wired_peripheral *peripheral = &peripherals[source];

uint8_t *buffer;
size_t len = ring_buf_put_claim(&peripheral->bus->state->tx_buf, &buffer,
sizeof(struct command_envelope));

if (len < sizeof(struct command_envelope)) {
LOG_WRN("No room to send command to the peripheral %d", source);
return -ENOSPC;
}

struct command_envelope env = {.source = source, .cmd = cmd};

env.crc = crc32_ieee((void *)&env, sizeof(env) - 4);
LOG_DBG("calculated a CRC for %d", env.crc);

memcpy(buffer, &env, sizeof(env));

ring_buf_put_finish(&peripheral->bus->state->tx_buf, len);
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_DEFAULT_INTERRUPT)
uart_irq_tx_enable(peripheral->bus->uart);
#else
k_work_submit(&wired_central_rx_work);
#endif

return 0;
}

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_bt_send_command,
.get_available_source_ids = split_central_bt_get_available_source_ids,
};

ZMK_SPLIT_TRANSPORT_CENTRAL_REGISTER(wired_central, &central_api);

static void publish_events_work(struct k_work *work) {
struct wired_bus_state *state = CONTAINER_OF(work, struct wired_bus_state, event_publish_work);

while (ring_buf_size_get(&state->rx_buf) >= sizeof(struct event_envelope)) {
struct event_envelope env;
size_t bytes_left = sizeof(struct event_envelope);

while (bytes_left > 0) {
size_t read = ring_buf_get(&state->rx_buf, (uint8_t *)&env + (sizeof(env) - bytes_left),
bytes_left);
bytes_left -= read;
}

LOG_HEXDUMP_DBG(&env, sizeof(env), "Env data");

// Exclude the trailing 4 bytes that contain the received CRC
uint32_t crc = crc32_ieee((uint8_t *)&env, sizeof(env) - 4);
if (crc != env.crc) {
LOG_WRN("Data corruption in received peripheral event, ignoring");
return;
}

zmk_split_transport_central_peripheral_event_handler(&wired_central, env.source, env.event);
}
}
Loading

0 comments on commit 47b7001

Please sign in to comment.