----------------------------------------------------------------------------------------------------------------------------开发板 久久派开发板eMMC8GBDDR4512MBu-bootu-boot 2022.04linux6.12rootfsbuildroot-2024.08----------------------------------------------------------------------------------------------------------------------------在《龙芯2k0300- 走马观碑组第21届智能汽车竞赛软硬件设计》中我们使用久久派开发板作为智能车主控板。前面已经完成了PWM、编码器、显示屏、摄像头等模块的移植这一节我们继续补充久久派板载按键驱动。久久派开发板上有两个用户按键其中KEY0对应UART2_TXD也就是GPIO44KEY1对应UART2_RXD也就是GPIO45。这两个按键可以用于智能车的发车、停车、模式切换、参数确认、调试触发等功能。不过内核驱动不应该直接处理“发车”这类业务逻辑驱动只负责把硬件按键转换成标准Linux input事件具体业务逻辑交给用户态程序处理。一、久久派KEY0/KEY1按键1.1 硬件连接关系久久派两个按键和龙芯2K0300的连接关系如下按键复用引脚GPIO默认电平按下电平说明KEY0UART2_TXDGPIO44高电平低电平低有效按键KEY1UART2_RXDGPIO45高电平低电平低有效按键也就是说按键没有按下时GPIO原始电平为高按下按键后GPIO原始电平被拉低。因此设备树中必须使用GPIO_ACTIVE_LOW描述按键极性。这样驱动通过gpiod_get_value_cansleep()读取GPIO时内核gpiod框架会自动把低有效电平转换为逻辑值未按下逻辑值0按下逻辑值1。1.2 为什么使用input子系统按键驱动有很多种实现方式比如字符设备用户态通过read()读取自定义结构体misc 设备用户态打开/dev/xxx读取按键状态input设备驱动上报标准EV_KEY事件。这里选择Linux input子系统原因是按键本身就是标准输入设备适合用EV_KEY事件描述用户态可以直接读取/dev/input/eventX后续也可以用evtest、libinput等工具调试驱动只负责上报按下、释放事件不和智能车业务逻辑耦合。本文中驱动注册的input设备名称为LS2K300 99Pi Keys默认键值映射如下按键Linux key code数值说明KEY0KEY_PROG1148可作为发车、确认等自定义功能键KEY1KEY_PROG2149可作为停车、模式切换等自定义功能键二、按键设备驱动2.1 创建驱动目录在driver目录下创建key_driver子目录zhengyangubuntu:~$ cd /opt/2k0300/loongson_2k300_lib/driver zhengyangubuntu:/opt/2k0300/loongson_2k300_lib/driver$ mkdir key_driver zhengyangubuntu:/opt/2k0300/loongson_2k300_lib/driver$ cd key_driver目录结构如下zhengyangubuntu:/opt/2k0300/loongson_2k300_lib/driver/key_driver$ tree . . ├── key_driver.c ├── Makefile └── README.md2.2key_driver.c按键驱动的核心思路如下通过设备树匹配compatible ls2k300-99pi-keys获取key0-gpios和key1-gpios将GPIO转换为IRQ同时监听上升沿和下降沿中断触发后启动延迟工作队列做消抖消抖完成后读取GPIO逻辑状态通过input_report_key()和input_sync()上报按键事件。驱动头文件和基本宏定义如下#include linux/gpio/consumer.h #include linux/input.h #include linux/interrupt.h #include linux/module.h #include linux/of.h #include linux/platform_device.h #include linux/slab.h #include linux/workqueue.h #define DRIVER_NAME ls2k300_99pi_keys #define DEFAULT_DEBOUNCE_MS 20单个按键运行状态使用struct key_button描述struct key_device; struct key_button { const char *name; struct gpio_desc *gpiod; int irq; unsigned int code; bool last_pressed; struct delayed_work work; struct key_device *parent; };整个双按键设备使用struct key_device描述struct key_device { struct device *dev; struct input_dev *input; struct key_button buttons[2]; unsigned int debounce_ms; };默认按键名称和键值static const char *const default_names[] { KEY0, KEY1, }; static const unsigned int default_codes[] { KEY_PROG1, KEY_PROG2, };2.2.1 读取按键状态由于设备树使用GPIO_ACTIVE_LOW所以gpiod_get_value_cansleep()返回的是逻辑值而不是原始电平。也就是说按键按下时返回1松开时返回0。static bool button_pressed(struct key_button *button) { int value gpiod_get_value_cansleep(button-gpiod); if (value 0) { dev_warn(button-parent-dev, failed to read %s GPIO: %d\n, button-name, value); return button-last_pressed; } return value ! 0; }2.2.2 上报按键事件只有当前状态和上一次状态不一致时才上报事件避免重复打印和重复上报。static void report_button_state(struct key_button *button) { bool pressed button_pressed(button); if (pressed button-last_pressed) return; button-last_pressed pressed; input_report_key(button-parent-input, button-code, pressed); input_sync(button-parent-input); dev_info(button-parent-dev, %s %s code%u\n, button-name, pressed ? pressed : released, button-code); }2.2.3 中断与消抖机械按键按下和释放时会出现抖动因此不能在中断中立即上报事件。这里中断处理函数只负责启动一个延迟工作真正读取GPIO和上报input事件在工作队列中完成。static void key_work(struct work_struct *work) { struct key_button *button container_of(to_delayed_work(work), struct key_button, work); report_button_state(button); } static irqreturn_t key_irq(int irq, void *data) { struct key_button *button data; mod_delayed_work(system_wq, button-work, msecs_to_jiffies(button-parent-debounce_ms)); return IRQ_HANDLED; }2.2.4 初始化单个按键setup_button()完成一个按键的GPIO获取、IRQ映射、input capability设置和中断注册。static int setup_button(struct key_device *keys, int index) { struct device *dev keys-dev; struct key_button *button keys-buttons[index]; char gpio_name[8]; int ret; snprintf(gpio_name, sizeof(gpio_name), key%d, index); button-name default_names[index]; button-code default_codes[index]; button-parent keys; INIT_DELAYED_WORK(button-work, key_work); if (dev-of_node) { of_property_read_string_index(dev-of_node, linux,key-names, index, button-name); of_property_read_u32_index(dev-of_node, linux,key-codes, index, button-code); } button-gpiod devm_gpiod_get(dev, gpio_name, GPIOD_IN); if (IS_ERR(button-gpiod)) { ret PTR_ERR(button-gpiod); dev_err(dev, failed to get %s GPIO: %d\n, gpio_name, ret); return ret; } button-irq gpiod_to_irq(button-gpiod); if (button-irq 0) { dev_err(dev, failed to map %s GPIO to IRQ: %d\n, button-name, button-irq); return button-irq; } button-last_pressed button_pressed(button); input_set_capability(keys-input, EV_KEY, button-code); ret devm_request_threaded_irq(dev, button-irq, NULL, key_irq, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | IRQF_ONESHOT, button-name, button); if (ret) { dev_err(dev, failed to request IRQ for %s: %d\n, button-name, ret); return ret; } dev_info(dev, %s registered on IRQ %d code %u initial%s\n, button-name, button-irq, button-code, button-last_pressed ? pressed : released); return 0; }2.2.5probe函数probe函数中分配驱动上下文、初始化input设备、初始化两个按键最后注册input设备。static int key_probe(struct platform_device *pdev) { struct key_device *keys; int ret; int i; dev_info(pdev-dev, probing %s input driver\n, DRIVER_NAME); keys devm_kzalloc(pdev-dev, sizeof(*keys), GFP_KERNEL); if (!keys) return -ENOMEM; keys-dev pdev-dev; keys-debounce_ms DEFAULT_DEBOUNCE_MS; device_property_read_u32(pdev-dev, debounce-interval-ms, keys-debounce_ms); keys-input devm_input_allocate_device(pdev-dev); if (!keys-input) return -ENOMEM; keys-input-name LS2K300 99Pi Keys; keys-input-phys ls2k300-99pi-keys/input0; keys-input-id.bustype BUS_HOST; for (i 0; i ARRAY_SIZE(keys-buttons); i) { ret setup_button(keys, i); if (ret) return ret; } ret input_register_device(keys-input); if (ret) { dev_err(pdev-dev, failed to register input device: %d\n, ret); return ret; } platform_set_drvdata(pdev, keys); dev_info(pdev-dev, LS2K300 99Pi key input driver ready: %s\n, keys-input-name); return 0; }2.2.6 设备树匹配表这里需要注意compatible设备树节点和驱动模块要匹配。static const struct of_device_id key_of_match[] { { .compatible ls2k300-99pi-keys }, { } }; MODULE_DEVICE_TABLE(of, key_of_match); static struct platform_driver key_driver { .probe key_probe, .remove key_remove, .driver { .name DRIVER_NAME, .of_match_table key_of_match, }, }; module_platform_driver(key_driver); MODULE_AUTHOR(zhengyang); MODULE_DESCRIPTION(LS2K300 99Pi GPIO key input driver); MODULE_LICENSE(GPL);2.3MakefileMakefile如下KERNELDIR ? /opt/2k0300/build-2k0300/workspace/linux-6.12 PWD : $(shell pwd) CROSS_COMPILE ? loongarch64-linux-gnu- ARCH : loongarch BUILD_DIR : build KO_DIR : ko obj-m : ls2k300_99pi_keys.o ls2k300_99pi_keys-y : key_driver.o all: prepare compile move_files prepare: mkdir -p $(BUILD_DIR) $(KO_DIR) echo key_driver: prepare build directories compile: echo key_driver: build kernel module make -C $(KERNELDIR) ARCH$(ARCH) CROSS_COMPILE$(CROSS_COMPILE) M$(PWD) modules move_files: find . -type f \ -not -path ./$(BUILD_DIR)/* -not -path ./$(KO_DIR)/* \ \( -name *.o -o -name *.mod -o -name *.mod.o -o -name *.mod.c -o -name .*.cmd -o -name modules.order -o -name Module.symvers \) \ ! -name *.ko -exec mv -t $(BUILD_DIR)/ {} find . -type f \ -not -path ./$(BUILD_DIR)/* -not -path ./$(KO_DIR)/* \ -name *.ko -exec cp -f {} $(KO_DIR)/ \; -exec rm -f {} \; clean: echo key_driver: clean build outputs make -C $(KERNELDIR) ARCH$(ARCH) CROSS_COMPILE$(CROSS_COMPILE) M$(PWD) clean rm -rf *.ko *.o *.mod *.mod.o *.mod.c *.symvers *.order .*.cmd .tmp_versions build ko .PHONY: all prepare compile move_files clean三、新增设备树节点3.1keys节点进入内核源码目录zhengyangubuntu:~$ cd /opt/2k0300/build-2k0300/workspace/linux-6.12修改arch/loongarch/boot/dts/ls2k300_99pi.dtsi在根节点/下增加keys节点keys { compatible ls2k300-99pi-keys; key0-gpios gpio 44 GPIO_ACTIVE_LOW; key1-gpios gpio 45 GPIO_ACTIVE_LOW; debounce-interval-ms 20; linux,key-codes 148 149; linux,key-names KEY0, KEY1; status okay; };说明compatible必须和驱动中的of_device_id一致key0-gpios对应GPIO44key1-gpios对应GPIO45GPIO_ACTIVE_LOW表示按键低有效debounce-interval-ms 20表示消抖时间为20mslinux,key-codes 148 149对应KEY_PROG1和KEY_PROG2。3.2 禁用UART2由于KEY0和KEY1占用了UART2_TXD和UART2_RXD所以必须禁用uart2避免串口和按键同时占用同一组引脚。uart2 { status disabled; };久久派默认可以把GPIO44和GPIO45作为普通GPIO使用因此这里不需要额外新增pinctrl节点把UART2_TXD/UART2_RXD切回GPIO。四、应用程序4.1 创建测试程序目录在example目录下创建key_appzhengyangubuntu:/opt/2k0300/loongson_2k300_lib/example$ mkdir key_app zhengyangubuntu:/opt/2k0300/loongson_2k300_lib/example$ cd key_app目录结构如下zhengyangubuntu:/opt/2k0300/loongson_2k300_lib/example/key_app$ tree . . ├── main.c └── Makefile4.2main.c用户态测试程序的逻辑如下默认查找input设备名称LS2K300 99Pi Keys如果找不到可以通过--device /dev/input/eventX手动指定打开/dev/input/eventX使用poll()等待input事件只打印KEY_PROG1和KEY_PROG2对应的按键事件。关键宏定义如下#include errno.h #include ctype.h #include fcntl.h #include linux/input.h #include poll.h #include stdio.h #include stdlib.h #include string.h #include unistd.h #define DEFAULT_DEVICE_NAME LS2K300 99Pi Keys #define DEFAULT_KEY0_CODE KEY_PROG1 #define DEFAULT_KEY1_CODE KEY_PROG2查找input设备名称LS2K300 99Pi Keys/* * brief 根据 input 设备名称查找对应 /dev/input/eventX 路径。 * param target_name 期望的 input 设备名称必须与驱动注册名称完全一致。 * param out 输出缓冲区用于保存 /dev/input/eventX。 * param out_size 输出缓冲区大小。 * return 0 表示找到设备负数表示未找到或读取失败。 */ static int find_input_device(const char *target_name, char *out, size_t out_size) { FILE *file; char line[512]; char name[128] ; char event[64] ; file fopen(/proc/bus/input/devices, r); if (!file) { fprintf(stderr, Failed to open /proc/bus/input/devices: %s\n, strerror(errno)); return -1; } while (fgets(line, sizeof(line), file)) { char *text trim(line); if (text[0] \0) { if (event[0] ! \0 strcmp(name, target_name) 0) { snprintf(out, out_size, /dev/input/%s, event); fclose(file); return 0; } reset_device_block(name, event); continue; } if (strncmp(text, N: Name, 8) 0) { read_quoted_value(text, name, sizeof(name)); continue; } if (strncmp(text, H: Handlers, 12) 0) { char *token strtok(text, \t); while (token) { if (strncmp(token, event, 5) 0) { snprintf(event, sizeof(event), %s, token); break; } token strtok(NULL, \t); } } } fclose(file); if (event[0] ! \0 strcmp(name, target_name) 0) { snprintf(out, out_size, /dev/input/%s, event); return 0; } return -1; }将键值转换为按键名称/* * brief 将按键键值转换为业务名称。 * param code input 子系统上报的 key code。 * return KEY0、KEY1 或 UNKNOWN 字符串。 */ static const char *key_name(unsigned short code) { if (code DEFAULT_KEY0_CODE) return KEY0; if (code DEFAULT_KEY1_CODE) return KEY1; return UNKNOWN; }将input事件值转换为动作名称/* * brief 将 input 按键状态值转换为动作名称。 * param value input_event.value0 松开1 按下2 重复。 * return release、press、repeat 或 unknown 字符串。 */ static const char *key_action(int value) { switch (value) { case 0: return release; case 1: return press; case 2: return repeat; default: return unknown; } }主循环读取input事件while (max_events 0 || event_count max_events) { struct pollfd pfd { .fd fd, .events POLLIN, }; struct input_event event; ssize_t nread; int ret; ret poll(pfd, 1, -1); if (ret 0) { if (errno EINTR) continue; fprintf(stderr, poll failed: %s\n, strerror(errno)); break; } nread read(fd, event, sizeof(event)); if (nread ! sizeof(event)) continue; if (event.type ! EV_KEY) continue; if (event.code ! DEFAULT_KEY0_CODE event.code ! DEFAULT_KEY1_CODE) continue; printf(%-4s %-7s code%u value%d time%ld.%06ld\n, key_name(event.code), key_action(event.value), event.code, event.value, (long)event.time.tv_sec, (long)event.time.tv_usec); event_count; }4.3MakefileTOOLCHAIN_DIR ? ../../cross_lib/loongarch64-linux-gnu-gcc13.3/bin CROSS_COMPILE ? $(TOOLCHAIN_DIR)/loongarch64-linux-gnu- ifeq ($(origin CC),default) CC : $(CROSS_COMPILE)gcc endif CFLAGS ? -Wall -Wextra -O2 TARGET : main all: $(CC) $(CFLAGS) -o $(TARGET) main.c clean: rm -rf *.o $(TARGET) .PHONY: all clean五、测试5.1 烧录设备树5.1.1 编译设备树如果需要单独编译设备树可以在driver目录使用统一脚本zhengyangubuntu:~$ cd /opt/2k0300/loongson_2k300_lib/driver zhengyangubuntu:/opt/2k0300/loongson_2k300_lib/driver$ ./build_driver.sh --target dtb脚本内部等价于在Linux内核目录执行zhengyangubuntu:~$ cd /opt/2k0300/build-2k0300/workspace/linux-6.12 zhengyangubuntu:/opt/2k0300/build-2k0300/workspace/linux-6.12$ source ../set_env.sh make dtbs V15.1.2 更新设备树将设备树拷贝到久久派并烧录到SPI Nor Flash的dtb分区zhengyangubuntu:/opt/2k0300/loongson_2k300_lib/driver$ ./build_driver.sh --target dtb --deploy root172.23.17.235脚本会把ls2k300_99pi_wifi.dtb上传到目标板/opt目录并在目标板执行[rootLS-GD opt]# dd if/opt/ls2k300_99pi_wifi.dtb of/dev/mtdblock3 bs1 [rootLS-GD opt]# sync烧录完成后重启开发板[rootLS-GD opt]# reboot5.2 安装驱动5.2.1 编译并部署驱动由于我们并没有将按键驱动源码放到内核源码树中因此需要单独编译安装。在ubuntu宿主机执行zhengyangubuntu:~$ cd /opt/2k0300/loongson_2k300_lib/driver zhengyangubuntu:/opt/2k0300/loongson_2k300_lib/driver$ ./build_driver.sh --target key --deploy root172.23.17.235脚本会完成以下工作调用内核外部模块构建流程生成ls2k300_99pi_keys.ko将模块复制到本地driver/key_driver/install/目录上传到开发板/lib/modules/$(uname -r)/目录执行depmod -a $(uname -r)更新模块依赖。部署日志示例key local install file updated: -rw-rw-r-- 1 zhengyang zhengyang 16664 5月 9 20:52 /opt/2k0300/loongson_2k300_lib/driver/key_driver/install/ls2k300_99pi_keys.ko Deploy /opt/2k0300/loongson_2k300_lib/driver/key_driver/install/ls2k300_99pi_keys.ko to root172.23.17.235:/lib/modules/6.12.0.lsgd/ls2k300_99pi_keys.ko ls2k300_99pi_keys.ko 100% 16KB 3.2MB/s 00:00 depmod: WARNING: could not open modules.builtin at /lib/modules/6.12.0.lsgd: No such file or directory depmod: WARNING: could not open modules.builtin.modinfo at /lib/modules/6.12.0.lsgd: No such file or directory这里depmod的输出是警告不是致命错误。含义是/lib/modules/$(uname -r)/目录缺少modules.builtin和modules.builtin.modinfo不影响当前外部模块通过modprobe加载。5.2.2 检查模块 alias在开发板检查模块信息[rootLS-GD ~]# modinfo /lib/modules/$(uname -r)/ls2k300_99pi_keys.ko | grep alias alias: of:N*T*Cls2k300-99pi-keysC* alias: of:N*T*Cls2k300-99pi-keys5.2.3 加载驱动手动加载驱动[rootLS-GD ~]# modprobe ls2k300_99pi_keys查看模块[rootLS-GD ~]# lsmod | grep ls2k300_99pi_keys ls2k300_99pi_keys 65536 0查看内核日志[rootLS-GD ~]# dmesg | grep -iE Keys正常情况下可以看到类似输出[ 698.012212] ls2k300_99pi_keys keys: probing ls2k300_99pi_keys input driver [ 698.019241] ls2k300_99pi_keys keys: debounce interval: 20 ms [ 698.025993] ls2k300_99pi_keys keys: input device name: LS2K300 99Pi Keys [ 698.034795] ls2k300_99pi_keys keys: KEY0 registered on IRQ 72 code 148 initialreleased [ 698.043222] ls2k300_99pi_keys keys: KEY1 registered on IRQ 73 code 149 initialreleased [ 698.051871] input: LS2K300 99Pi Keys as /devices/platform/keys/input/input0 [ 698.067180] ls2k300_99pi_keys keys: LS2K300 99Pi key input driver ready: LS2K300 99Pi Keys5.3 验证 input 设备查看/proc/bus/input/devices[rootLS-GD ~]# cat /proc/bus/input/devices正常情况下可以看到类似内容[rootLS-GD 6.12.0.lsgd]# cat /proc/bus/input/devices I: Bus0019 Vendor0000 Product0000 Version0000 N: NameLS2K300 99Pi Keys P: Physls2k300-99pi-keys/input0 S: Sysfs/devices/platform/keys/input/input0 U: Uniq H: Handlerskbd event0 B: PROP0 B: EV3 B: KEY300000 0 0这里重点关注NameLS2K300 99Pi KeysHandlers中存在eventX。5.4 应用程序测试5.4.1 编译、部署并运行在宿主机example目录执行zhengyangubuntu:~$ cd /opt/2k0300/loongson_2k300_lib/example zhengyangubuntu:/opt/2k0300/loongson_2k300_lib/example$ ./build_deploy_run.sh --app key_app --deploy root172.23.17.235也可以只编译部署不立即运行zhengyangubuntu:/opt/2k0300/loongson_2k300_lib/example$ ./build_deploy_run.sh --app key_app --deploy root172.23.17.235 --no-run然后在开发板上运行[rootLS-GD opt]# ./key_app Listening on /dev/input/event0 (LS2K300 99Pi Keys) KEY0148 KEY1149, press CtrlC to stop按下和松开KEY0、KEY1后可以看到类似输出KEY0 press code148 value1 time123.456789 KEY0 release code148 value0 time123.556789 KEY1 press code149 value1 time125.123456 KEY1 release code149 value0 time125.2234565.4.2 手动指定 event 设备如果程序没有自动找到input设备可以手动指定[rootLS-GD opt]# ./key_app --device /dev/input/event0也可以限制打印事件数量[rootLS-GD opt]# ./key_app --count 45.5 常见问题5.5.1 驱动加载后没有 probe 日志先确认设备树中是否存在keys节点[rootLS-GD ~]# grep -aR ls2k300-99pi-keys /proc/device-tree 2/dev/null /proc/device-tree/keys/compatible:ls2k300-99pi-keys再确认驱动alias是否匹配[rootLS-GD ~]# modinfo /lib/modules/$(uname -r)/ls2k300_99pi_keys.ko | grep alias alias: of:N*T*Cls2k300-99pi-keysC* alias: of:N*T*Cls2k300-99pi-keys可以看到模块alias是ls2k300-99pi-keys和设备树一致。5.5.2 找不到 input 设备检查驱动是否加载[rootLS-GD ~]# lsmod | grep ls2k300_99pi_keys检查platform device和driver[rootLS-GD ~]# ls /sys/bus/platform/devices | grep -i key keys [rootLS-GD ~]# ls /sys/bus/platform/drivers/ls2k300_99pi_keys bind keys module uevent unbind如果有device也有driver但是没有probe日志通常就是compatible或模块alias不匹配。5.5.3 按键一直是按下状态或状态相反久久派KEY0/KEY1是低有效按键设备树必须写key0-gpios gpio 44 GPIO_ACTIVE_LOW; key1-gpios gpio 45 GPIO_ACTIVE_LOW;如果误写成GPIO_ACTIVE_HIGH按键逻辑会反过来。六、代码下载loongson_2k300_lib参考文章[1]Linux input子系统文档[2] 龙芯2K0300数据手册[3] 龙芯2K0300处理器用户手册