drivers: input: Add CH9350L USB keyboard driver

Adds a driver that allows using CH9350 as a USB keyboard interface chip

Signed-off-by: Camille BAUD <mail@massdriver.space>
This commit is contained in:
Camille BAUD
2026-01-08 22:00:08 +01:00
committed by Anas Nashif
parent 0c97804b3d
commit fd9b0b81b7
5 changed files with 481 additions and 0 deletions

View File

@@ -10,6 +10,7 @@ zephyr_library_sources_ifdef(CONFIG_INPUT_ANALOG_AXIS_SETTINGS input_analog_axis
zephyr_library_sources_ifdef(CONFIG_INPUT_BFLB_IRX input_bflb_irx.c)
zephyr_library_sources_ifdef(CONFIG_INPUT_CAP12XX input_cap12xx.c)
zephyr_library_sources_ifdef(CONFIG_INPUT_CF1133 input_cf1133.c)
zephyr_library_sources_ifdef(CONFIG_INPUT_CH9350L input_ch9350l.c)
zephyr_library_sources_ifdef(CONFIG_INPUT_CHSC5X input_chsc5x.c)
zephyr_library_sources_ifdef(CONFIG_INPUT_CHSC6X input_chsc6x.c)
zephyr_library_sources_ifdef(CONFIG_INPUT_CST816S input_cst816s.c)

View File

@@ -11,6 +11,7 @@ source "drivers/input/Kconfig.analog_axis"
source "drivers/input/Kconfig.bflb"
source "drivers/input/Kconfig.cap12xx"
source "drivers/input/Kconfig.cf1133"
source "drivers/input/Kconfig.ch9350l"
source "drivers/input/Kconfig.chsc5x"
source "drivers/input/Kconfig.chsc6x"
source "drivers/input/Kconfig.cst816s"

View File

@@ -0,0 +1,23 @@
# Copyright (c) 2026 MASSDRIVER EI (massdriver.space)
# SPDX-License-Identifier: Apache-2.0
config INPUT_CH9350L
bool "WinChipHead CH9350L USB HID to UART control chip"
default y
depends on DT_HAS_WCH_CH9350L_ENABLED
depends on SERIAL_SUPPORT_INTERRUPT
select UART
select UART_INTERRUPT_DRIVEN
help
This option enables the driver for WinChipHead CH9350L USB HID to UART control chips.
if INPUT_CH9350L
config INPUT_CH9350L_FRAME_COUNT
int "Number of frames allocatable"
range 1 128
default 2
help
Number of input frames queueable at once.
endif # INPUT_CH9350L

View File

@@ -0,0 +1,414 @@
/*
* Copyright (c) 2026 MASSDRIVER EI (massdriver.space)
* SPDX-License-Identifier: Apache-2.0
*/
#define DT_DRV_COMPAT wch_ch9350l
#include <zephyr/device.h>
#include <zephyr/input/input.h>
#include <zephyr/kernel.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(input_ch9350l, CONFIG_INPUT_LOG_LEVEL);
/* The theorical maximum is 72 */
#define CH9350L_FRAME_SIZE_MAX 72
#define CH9350L_FRAME_SIZE_MIN 8
#define CH9350L_WAIT_TIMEOUT_MS 100
#define CH9350L_READBUF_SIZE 16
#define CH9350L_FRAME_HEAD_0 0x57
#define CH9350L_FRAME_HEAD_1 0xAB
#define CH9350L_FRAME_HEAD_KEY_0 0x83
#define CH9350L_FRAME_HEAD_KEY_1 0x88
#define CH9350L_FRAME_HEAD_OFF 3
#define CH9350L_FRAME_LENGTH_OFF 4
#define CH9350L_FRAME_VALUE_MAX (CH9350L_FRAME_SIZE_MAX - CH9350L_FRAME_HEAD_OFF - 4)
#define CH9350L_FRAME_TYPE_MASK GENMASK(5, 4)
#define CH9350L_FRAME_TYPE_POS 4
#define CH9350L_FRAME_TYPE_OTHER 0
#define CH9350L_FRAME_TYPE_KB 1
#define CH9350L_FRAME_TYPE_MOUSE 2
#define CH9350L_FRAME_TYPE_MM 3
#define CH9350L_FRAME_MOUSE_BUTTON_BYTE 0
#define CH9350L_FRAME_MOUSE_X_BYTE 1
#define CH9350L_FRAME_MOUSE_Y_BYTE 3
#define CH9350L_FRAME_MOUSE_RELMID 0x7FFF
#define CH9350L_FRAME_MOUSE_RELNEG 0x8000
#define CH9350L_RAWMOUSE_TO_REL(_val) (_val > CH9350L_FRAME_MOUSE_RELMID ? \
-(CH9350L_FRAME_MOUSE_RELMID - (const int16_t)(_val - CH9350L_FRAME_MOUSE_RELNEG)) \
: (const int16_t)_val)
#define CH9350L_FRAME_MOUSE_BTN_LEFT 0x1
#define CH9350L_FRAME_MOUSE_BTN_RIGHT 0x2
#define CH9350L_FRAME_MOUSE_BTN_MID 0x4
#define CH9350L_FRAME_MOUSE_BTN_4 0x8
#define CH9350L_FRAME_MOUSE_BTN_5 0x10
#define CH9350L_FRAME_MOUSE_BTN_6 0x20
#define CH9350L_FRAME_MOUSE_BTN_7 0x40
#define CH9350L_FRAME_MOUSE_BTN_8 0x80
static const uint8_t ch9350l_enable_status_frame[] = {
0x57, 0xab, 0x12, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x20
};
static const uint8_t ch9350l_disable_status_frame[] = {
0x57, 0xab, 0x12, 0x00, 0x00, 0x00, 0x00, 0xff, 0x80, 0x00, 0x20
};
static const uint8_t ch9350l_valid_start_of_status_frame[] = { 0x57, 0xab, 0x82 };
struct ch9350l_frame {
uint8_t data[CH9350L_FRAME_SIZE_MAX - CH9350L_FRAME_HEAD_OFF];
size_t size;
};
#define CH9350L_MSGQBUF_SIZE (CONFIG_INPUT_CH9350L_FRAME_COUNT * sizeof(struct ch9350l_frame))
#define CH9350L_MSGQBUF_TYPE char __aligned(4)
struct ch9350l_data {
uint8_t frame_buffer[CH9350L_FRAME_SIZE_MAX];
size_t frame_buffer_size;
bool frame_started;
uint8_t last_kb_values[CH9350L_FRAME_VALUE_MAX];
uint8_t last_mouse_btns;
const struct device *dev;
struct k_work work;
struct k_msgq msgq;
CH9350L_MSGQBUF_TYPE msgq_buffer[CH9350L_MSGQBUF_SIZE];
};
struct ch9350l_config {
const struct device *uart;
const int *kb_codemap;
const size_t kb_codemap_len;
const int *mouse_codemap;
const size_t mouse_codemap_len;
};
static uint32_t ch9350l_kb_map(const struct device *dev, uint32_t code)
{
const struct ch9350l_config *config = dev->config;
const size_t code_map_len = config->kb_codemap_len / 2;
for (size_t i = 0; i < code_map_len; i++) {
if (config->kb_codemap[i * 2] == code) {
return config->kb_codemap[i * 2 + 1];
}
}
return code;
}
static void ch9350l_kb(const struct device *dev, const uint8_t *values, uint8_t length)
{
struct ch9350l_data *data = dev->data;
uint8_t *lkbv = data->last_kb_values;
bool found;
for (size_t j = 0; j < CH9350L_FRAME_VALUE_MAX; j++) {
found = false;
for (size_t i = 0; i < length; i++) {
if (lkbv[j] == values[i]) {
found = true;
}
}
if (!found && lkbv[j] != 0) {
if (input_report(dev, INPUT_EV_KEY,
ch9350l_kb_map(dev, lkbv[j]), 0, true, K_FOREVER)) {
LOG_ERR("Input failed to be enqueued");
}
}
}
for (size_t i = 0; i < length; i++) {
found = false;
for (size_t j = 0; j < CH9350L_FRAME_VALUE_MAX; j++) {
if (lkbv[j] == values[i]) {
found = true;
}
}
if (!found && values[i] != 0) {
if (input_report(dev, INPUT_EV_KEY,
ch9350l_kb_map(dev, values[i]), 1, true, K_FOREVER)) {
LOG_ERR("Input failed to be enqueued");
}
}
}
memcpy(lkbv, values, length);
memset(&lkbv[length], 0, CH9350L_FRAME_VALUE_MAX - length);
}
static uint32_t ch9350l_mouse_map(const struct device *dev, uint8_t code)
{
const struct ch9350l_config *config = dev->config;
const size_t code_map_len = config->mouse_codemap_len / 2;
for (size_t i = 0; i < code_map_len; i++) {
if (config->mouse_codemap[i * 2] == code) {
return config->mouse_codemap[i * 2 + 1];
}
}
return code;
}
static void ch9350l_mouse(const struct device *dev, const uint8_t *values, uint8_t length)
{
struct ch9350l_data *data = dev->data;
const uint8_t button = values[CH9350L_FRAME_MOUSE_BUTTON_BYTE];
const uint16_t raw_x = sys_get_le16(&values[CH9350L_FRAME_MOUSE_X_BYTE]);
const uint16_t raw_y = sys_get_le16(&values[CH9350L_FRAME_MOUSE_Y_BYTE]);
const int16_t x = CH9350L_RAWMOUSE_TO_REL(raw_x);
const int16_t y = CH9350L_RAWMOUSE_TO_REL(raw_y);
input_report(dev, INPUT_EV_REL, INPUT_REL_X, x, true, K_FOREVER);
input_report(dev, INPUT_EV_REL, INPUT_REL_Y, y, true, K_FOREVER);
for (size_t i = 0; i < 8; i++) {
if (button & BIT(i) && !(data->last_mouse_btns & BIT(i))) {
if (input_report(dev, INPUT_EV_DEVICE,
ch9350l_mouse_map(dev, BIT(i)), 1, true, K_FOREVER)) {
LOG_ERR("Input failed to be enqueued");
}
} else if (data->last_mouse_btns & BIT(i) && !(button & BIT(i))) {
if (input_report(dev, INPUT_EV_DEVICE,
ch9350l_mouse_map(dev, BIT(i)), 0, true, K_FOREVER)) {
LOG_ERR("Input failed to be enqueued");
}
} else {
/* Dont care about other cases */
}
}
data->last_mouse_btns = button;
}
static void ch9350l_input_work_handler(struct k_work *item)
{
struct ch9350l_data *data = CONTAINER_OF(item, struct ch9350l_data, work);
struct ch9350l_frame frame;
uint8_t fd_label;
uint8_t *fd_value;
uint8_t fd_sum;
uint8_t sum;
while (k_msgq_get(&data->msgq, &frame, K_NO_WAIT) == 0) {
sum = 0;
fd_label = frame.data[0];
fd_value = &frame.data[1];
fd_sum = frame.data[frame.size - 1];
for (int i = 0; i < frame.size - 2; i++) {
sum += fd_value[i];
}
if (sum != fd_sum) {
LOG_ERR("Frame checksum is invalid");
continue;
}
switch ((fd_label & CH9350L_FRAME_TYPE_MASK) >> CH9350L_FRAME_TYPE_POS) {
case CH9350L_FRAME_TYPE_KB:
ch9350l_kb(data->dev, fd_value,
min(frame.size - 3, CH9350L_FRAME_VALUE_MAX));
continue;
case CH9350L_FRAME_TYPE_MOUSE:
ch9350l_mouse(data->dev, fd_value,
min(frame.size - 3, CH9350L_FRAME_VALUE_MAX));
continue;
case CH9350L_FRAME_TYPE_MM:
default:
LOG_ERR("Unknown or unsupported input type");
continue;
}
}
}
static int ch9350l_queue_frame(struct ch9350l_data *dev_data, uint8_t *data, size_t size)
{
int ret;
struct ch9350l_frame frame = {
.size = size,
};
memcpy(frame.data, data, size);
ret = k_msgq_put(&dev_data->msgq, &frame, K_NO_WAIT);
if (ret < 0) {
LOG_WRN("Frame dropped, queue full");
}
k_work_submit(&dev_data->work);
return ret;
}
static uint8_t ch9350l_is_valid_frame(uint8_t *data, size_t size)
{
const uint8_t fd_id = data[2];
const uint8_t fd_length = data[3];
/* Too small to be valid */
if (size < CH9350L_FRAME_SIZE_MIN) {
return 0;
}
/* Drop non-input frames */
if (fd_id != CH9350L_FRAME_HEAD_KEY_0 && fd_id != CH9350L_FRAME_HEAD_KEY_1) {
return 0;
}
/* We don't have the full frame yet */
if (fd_length > (size - CH9350L_FRAME_LENGTH_OFF)) {
return 0;
}
return fd_length;
}
static void ch9350l_input_callback(const struct device *dev_uart, void *user_data)
{
struct ch9350l_data *data = (struct ch9350l_data *)user_data;
uint8_t read_buffer[CH9350L_READBUF_SIZE];
int read;
uint8_t frame_size;
uart_irq_update(dev_uart);
if (!uart_irq_rx_ready(dev_uart)) {
return;
}
while (true) {
read = uart_fifo_read(dev_uart, read_buffer, CH9350L_READBUF_SIZE);
if (read <= 0) {
break;
}
if (data->frame_buffer_size + read >= CH9350L_FRAME_SIZE_MAX) {
LOG_ERR("Maximum frame size exceeded");
data->frame_started = false;
data->frame_buffer_size = 0;
continue;
}
memcpy(&data->frame_buffer[data->frame_buffer_size], read_buffer, read);
data->frame_buffer_size += read;
for (size_t offset = max((int)data->frame_buffer_size - read - 2, 0);
offset < (data->frame_buffer_size - 1); offset++) {
if (data->frame_buffer[offset] != CH9350L_FRAME_HEAD_0
|| data->frame_buffer[offset+1] != CH9350L_FRAME_HEAD_1) {
continue;
}
if (data->frame_started) {
frame_size = ch9350l_is_valid_frame(data->frame_buffer,
data->frame_buffer_size);
if (frame_size) {
ch9350l_queue_frame(data,
&data->frame_buffer[CH9350L_FRAME_LENGTH_OFF],
frame_size);
}
}
data->frame_started = true;
memcpy(data->frame_buffer, &data->frame_buffer[offset],
data->frame_buffer_size - offset);
data->frame_buffer_size = data->frame_buffer_size - offset;
break;
}
}
if (data->frame_started) {
frame_size = ch9350l_is_valid_frame(data->frame_buffer, data->frame_buffer_size);
if (frame_size) {
ch9350l_queue_frame(data, &data->frame_buffer[CH9350L_FRAME_LENGTH_OFF],
frame_size);
data->frame_started = false;
data->frame_buffer_size = 0;
}
}
if (read < 0) {
LOG_ERR("Error reading UART");
}
}
static int ch9350l_init(struct device const *dev)
{
const struct ch9350l_config *config = dev->config;
struct ch9350l_data *data = dev->data;
k_timepoint_t end_timeout =
sys_timepoint_calc(K_MSEC(CH9350L_WAIT_TIMEOUT_MS));
size_t check_p = 0;
int ret = 0;
char c;
data->dev = dev;
data->frame_buffer_size = 0;
data->frame_started = false;
k_work_init(&data->work, ch9350l_input_work_handler);
k_msgq_init(&data->msgq, data->msgq_buffer, sizeof(struct ch9350l_frame),
CONFIG_INPUT_CH9350L_FRAME_COUNT);
if (!device_is_ready(config->uart)) {
LOG_ERR("UART device not ready");
return -ENODEV;
}
for (int i = 0; i < sizeof(ch9350l_enable_status_frame); i++) {
uart_poll_out(config->uart, ch9350l_enable_status_frame[i]);
}
while ((ret == 0 || ret == -1) && !sys_timepoint_expired(end_timeout)
&& check_p < ARRAY_SIZE(ch9350l_valid_start_of_status_frame)) {
ret = uart_poll_in(config->uart, &c);
if (c == ch9350l_valid_start_of_status_frame[check_p]) {
check_p++;
}
}
if (check_p != ARRAY_SIZE(ch9350l_valid_start_of_status_frame)) {
LOG_ERR("CH9350L not detected");
return -ENXIO;
}
/* Flush FIFO if applicable */
while (uart_poll_in(config->uart, &c)) {
};
for (int i = 0; i < sizeof(ch9350l_disable_status_frame); i++) {
uart_poll_out(config->uart, ch9350l_disable_status_frame[i]);
}
ret = uart_irq_callback_user_data_set(config->uart, ch9350l_input_callback, data);
if (ret < 0) {
LOG_ERR("Couldn't set UART callback");
return ret;
}
uart_irq_rx_enable(config->uart);
return ret;
}
#define CH9350L_DEFINE(inst) \
static struct ch9350l_data ch9350l_data_##inst = {0}; \
\
static struct ch9350l_config const ch9350l_config_##inst = { \
.uart = DEVICE_DT_GET(DT_INST_BUS(inst)), \
.kb_codemap = COND_CODE_1(DT_NODE_HAS_PROP(DT_DRV_INST(inst), kb_codemap), \
((int []) DT_INST_PROP(inst, kb_codemap)), NULL), \
.kb_codemap_len = DT_INST_PROP_LEN_OR(inst, kb_codemap, 0), \
.mouse_codemap = COND_CODE_1(DT_NODE_HAS_PROP(DT_DRV_INST(inst), mouse_codemap),\
((int []) DT_INST_PROP(inst, mouse_codemap)), NULL), \
.mouse_codemap_len = DT_INST_PROP_LEN_OR(inst, mouse_codemap, 0), \
}; \
\
DEVICE_DT_INST_DEFINE(inst, ch9350l_init, NULL, &ch9350l_data_##inst, \
&ch9350l_config_##inst, POST_KERNEL, CONFIG_INPUT_INIT_PRIORITY, \
NULL); \
BUILD_ASSERT((DT_INST_PROP_LEN_OR(inst, kb_code_map, 0) & 0x1) == 0, \
"kb-codemap is not of a valid size"); \
BUILD_ASSERT((DT_INST_PROP_LEN_OR(inst, mouse_code_map, 0) & 0x1) == 0, \
"mouse-codemap is not of a valid size");
DT_INST_FOREACH_STATUS_OKAY(CH9350L_DEFINE)

View File

@@ -0,0 +1,42 @@
# Copyright (c) 2026 MASSDRIVER EI (massdriver.space)
# SPDX-License-Identifier: Apache-2.0
description: |
WinChipHead CH9350L USB HID to UART control chip.
This supports State 0.
&uart0 {
status = "okay";
usb_kb {
compatible = "wch,ch9350l";
status = "okay";
kb-codemap = <
4 INPUT_KEY_Q
5 INPUT_KEY_B
6 INPUT_KEY_C
>;
mouse-codemap = <
1 INPUT_BTN_LEFT
2 INPUT_BTN_RIGHT
4 INPUT_BTN_MIDDLE
>;
};
};
compatible: "wch,ch9350l"
include: [base.yaml, uart-device.yaml]
properties:
kb-codemap:
type: array
description: |
Maps a keyboard key code to a input code by pair of 2 when needed.
mouse-codemap:
type: array
description: |
Maps a mouse button code to a input code by pair of 2 when needed.