Skip to content

Commit

Permalink
feat(split): wired split over serial support (WIP 2023-12-18)
Browse files Browse the repository at this point in the history
  • Loading branch information
xudongzheng committed Dec 19, 2023
1 parent a593c72 commit b7d63fb
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 1 deletion.
6 changes: 5 additions & 1 deletion app/src/split/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@

if (CONFIG_ZMK_SPLIT_BLE)
add_subdirectory(bluetooth)
endif()
endif()

if (CONFIG_ZMK_SPLIT_SERIAL)
add_subdirectory(serial)
endif()
5 changes: 5 additions & 0 deletions app/src/split/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ config ZMK_SPLIT_BLE
select BT_USER_PHY_UPDATE
select BT_AUTO_PHY_UPDATE

config ZMK_SPLIT_SERIAL
bool "Serial"
select RING_BUFFER

endchoice

config ZMK_SPLIT_PERIPHERAL_HID_INDICATORS
Expand All @@ -30,3 +34,4 @@ config ZMK_SPLIT_PERIPHERAL_HID_INDICATORS
endif

rsource "bluetooth/Kconfig"
rsource "serial/Kconfig"
6 changes: 6 additions & 0 deletions app/src/split/serial/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copyright (c) 2023 The ZMK Contributors
# SPDX-License-Identifier: MIT

if (CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
target_sources(app PRIVATE central.c)
endif()
25 changes: 25 additions & 0 deletions app/src/split/serial/Kconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
if ZMK_SPLIT && ZMK_SPLIT_SERIAL

menu "Serial Transport"

config ZMK_SPLIT_SERIAL_UART
bool "Serial over UART"
default ZMK_SPLIT_SERIAL

config ZMK_SPLIT_SERIAL_UART_POLLING
bool "Serial over UART Polling API"
default n
# TODO TODO TODO once on 3.5/3.6, replace with the following
# default ZMK_SPLIT_SERIAL && (DT_HAS_RASPBERRYPI_PICO_UART_PIO_ENABLED || BOARD_NATIVE_SIM)

config ZMK_SPLIT_SERIAL_CDC_ACM
bool "Serial over USB CDC ACM"
default n

config ZMK_SPLIT_SERIAL_BITMAP
bool "Serial bitmap protocol"
default ZMK_SPLIT_SERIAL

endmenu

endif
274 changes: 274 additions & 0 deletions app/src/split/serial/central.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/crc.h>
#include <zephyr/sys/ring_buffer.h>

#include <zephyr/sys/byteorder.h>
#include <zephyr/bluetooth/bluetooth.h>

#include <zmk/events/position_state_changed.h>

// TODO TODO TODO
LOG_MODULE_REGISTER(slicemk);

#define SERIAL_MSG_PREFIX "UarT"

// The serial protocol is defined for payloads of up to 254 bytes. TODO TODO
// TODO len 255 designates another part to come. support for that can be
// implemented later.
#define SERIAL_BUF_SIZE 300

#ifdef CONFIG_ZMK_SPLIT_SERIAL_UART_POLLING
K_THREAD_STACK_DEFINE(uart_tx_wq_stack, 256);
struct k_work_q uart_tx_wq;
#endif

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;

#ifdef CONFIG_ZMK_SPLIT_SERIAL_UART_POLLING
bool poll;
struct k_work tx_work;
struct k_timer rx_timer;
#endif
};

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_POLLING
.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)

#define POSITION_STATE_DATA_LEN 16
static uint8_t position_state[POSITION_STATE_DATA_LEN];
static uint8_t changed_positions[POSITION_STATE_DATA_LEN];

K_MSGQ_DEFINE(peripheral_event_msgq, sizeof(struct zmk_position_state_changed), 10, 4);

void peripheral_event_work_callback(struct k_work *work) {
struct zmk_position_state_changed ev;
while (k_msgq_get(&peripheral_event_msgq, &ev, K_NO_WAIT) == 0) {
LOG_DBG("Trigger key position state change for %d", ev.position);
ZMK_EVENT_RAISE(new_zmk_position_state_changed(ev));
}
}

// TODO TODO TODO this was copied from split bt. can go into
// app/src/split/split.c
K_WORK_DEFINE(peripheral_event_work, peripheral_event_work_callback);

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()};

k_msgq_put(&peripheral_event_msgq, &ev, K_NO_WAIT);
k_work_submit(&peripheral_event_work);
}
}
}
}

static void serial_handle(uint32_t cmd, uint8_t *data, uint8_t len) {
switch (cmd) {
// Handle bitmap split transformed (bst) version 0.
case 0x62737400:
serial_handle_bitmap(data, len);
break;

default:
LOG_ERR("Received unexpected UART command 0x%08x", cmd);
break;
}
}

static bool serial_callback_tx(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_callback_rx_internal(struct serial_device *sd) {
// Continue processing data as long as the buffer exceeds the header length
// (13 bytes).
uint8_t data[512];
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(cmd, &data[13], len);
} else {
LOG_ERR("received UART message with invalid CRC32 checksum");
}
}
}

static void serial_callback_rx(struct serial_device *sd) {
uint8_t c;
while (uart_fifo_read(sd->dev, &c, 1) == 1) {
ring_buf_put(&sd->rx_rb, &c, 1);
}
serial_callback_rx_internal(sd);
}

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_callback_tx(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_callback_rx(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_POLLING
if (sd->poll) {
k_work_submit_to_queue(&uart_tx_wq, &sd->tx_work);
return;
}
#endif

uart_irq_tx_enable(sd->dev);
}

#ifdef CONFIG_ZMK_SPLIT_SERIAL_UART_POLLING

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));
}
serial_callback_rx_internal(sd);
}

#endif

static int serial_init() {
#ifdef CONFIG_ZMK_SPLIT_SERIAL_UART_POLLING
// TODO TODO TODO define kconfig for prio. nrf52 max 14. want this lower
// than lowprio_wq since it can block.
struct k_work_queue_config uart_tx_cfg = {.name = "uart_tx_wq"};
k_work_queue_start(&uart_tx_wq, uart_tx_wq_stack, K_THREAD_STACK_SIZEOF(uart_tx_wq_stack), 14,
&uart_tx_cfg);
#endif

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);

#ifdef CONFIG_ZMK_SPLIT_SERIAL_UART_POLLING
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

// TODO TODO TODO iirc need to check return function in zephyr 3.5
uart_irq_callback_user_data_set(sd->dev, serial_callback, sd);
uart_irq_rx_enable(sd->dev);
}

return 0;
}

SYS_INIT(serial_init, APPLICATION, 0);

0 comments on commit b7d63fb

Please sign in to comment.