ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux下松下CF-SV圆盘滚轮驱动实现:基于uinput的用户态方案

Linux下松下CF-SV圆盘滚轮驱动实现:基于uinput的用户态方案 使用松下 Let’s Note CF-SV 系列的朋友可能都有这个感受Windows 下官方驱动把触摸板下方那个圆盘滚轮调教得非常好用手指绕着圆盘转就可以顺滑滚动网页、表格和长文档。可一旦切到 Linux这个圆盘就变成一个“装饰品”手指怎么转都没反应。查了不少资料发现要么是零散的帖子要么只讲 Windows 下的方案Linux 下能用的圆盘滚轮驱动几乎没有现成的。本文就来完整拆解一个可行的用户态驱动方案通过读取触摸板原始事件识别手指在圆盘区域内的旋转手势再借助 Linux 的 uinput 机制向系统注入标准滚轮事件。整个过程不需要编写内核模块只要具备 Linux 基础、能运行 Python 脚本的开发者都可以照着做。下文会覆盖原理、环境准备、完整代码、调试方法和发布建议希望对同样在 CF-SV 系列上使用 Linux 的朋友有帮助。1. 背景与核心概念1.1 圆盘滚轮是什么松下 CF-SV 系列属于 Let’s Note 产品线定位商务轻薄本它的触摸板设计比较特殊——触摸板下方集成了一整块圆形感应区域也就是“圆盘滚轮”。在 Windows 下手指在圆盘表面划动时系统会把它识别为鼠标滚轮的滚动操作划动方向、速度和滚动方向之间还有一套平滑映射。从硬件角度看这个圆盘并不是独立的输入设备而是触摸板的一部分。触摸板本身通过 I2C 或 PS/2 接口连接主板内核把它注册为标准的触摸板设备上报的依然是ABS_X、ABS_Y、ABS_MT_POSITION_X、ABS_MT_POSITION_Y这类坐标事件。所谓的“圆盘滚轮驱动”真正的任务是识别出“手指落在圆盘区域内”和“手指在圆盘上发生旋转”这两个信息并转换成滚轮事件。1.2 为什么 Linux 下默认用不了Linux 内核和桌面环境对触摸板的支持已经相当完善常见的 ELAN、Synaptics、ALPS 触摸板都能被hid-multitouch或psmouse驱动识别。但问题在于松下这个圆盘不是标准触摸板协议里的概念。内核只知道这是一个触摸板并不知道屏幕上的那个圆圈标记代表什么也不理解“绕着圆盘转”这个手势。libinput虽然提供了多种手势识别能力比如双指滚动、自然滚动、点击拖拽但没有针对松下圆盘滚轮的适配。第三方输入驱动没有这一层逻辑桌面环境自然只能把触摸板当作普通触摸板处理。因此我们需要的不是让内核“认识”松下硬件而是在用户态自己补充这一层手势转换逻辑。1.3 用户态驱动的整体思路Linux 输入子系统是一个开放链路。触摸板产生的事件会进入内核并通过/dev/input/eventX暴露给用户态libinput、Xorg、Wayland 合成器等都是从这个节点读取事件。我们可以在应用层插入一个“翻译器”从/dev/input/eventX读取触摸板原始事件。通过坐标判断手指是否落在圆盘区域内。计算手指位置相对于圆盘中心的角度变化。当角度累积到一定阈值就通过/dev/uinput创建一个虚拟滚轮设备并写入标准滚轮事件。这样做的好处是不涉及内核模块编译门槛低。不依赖特定内核版本只要输入子系统的接口稳定即可。虚拟滚轮设备对上层应用完全透明浏览器、终端、文档阅读器都能直接响应。2. 环境准备与版本说明2.1 操作系统与内核版本本文示例以 Ubuntu 22.04 LTS 为主Debian 12、Fedora 38 等主流发行版同样适用。实现方案依赖的标准接口比较稳定只要内核版本在 5.x 以上理论上差异都不大。如果你的系统还在使用旧内核建议先升级到发行版维护的长期内核版本。2.2 需要安装的工具和 Python 库为了完成开发和调试需要准备以下工具工具用途evtest查看/dev/input/eventX设备列表和事件流转python3运行驱动守护进程python3-evdev或evdev读取输入事件、操作 uinput 虚拟设备python3-yaml加载配置文件方便校准坐标和阈值安装命令如下。Ubuntu/Debian 系sudo apt update sudo apt install -y evtest python3 python3-pip python3-yaml pip3 install evdevFedora / RHEL 系sudo dnf install -y evtest python3 python3-pip pip3 install evdev PyYAML如果你的发行版对/dev/uinput有单独的用户组权限控制还需要确认当前用户是否属于相应组通常可以临时用sudo运行验证后面再通过 udev 规则固定权限。2.3 确认触摸板设备节点在开始写驱动之前先用evtest列出当前系统识别的输入设备evtest输出会列出input device列表例如Available devices: /dev/input/event4: ELAN0672:00 04F3:31A8 Touchpad /dev/input/event5: AT Translated Set 2 keyboardCF-SV 系列不同型号触摸板芯片可能不一样可能是ELAN、ALPS或Synaptics。请根据自己的evtest输出找到触摸板对应的事件节点。不同批次、不同型号的设备名不同不要直接照搬别人教程里的/dev/input/eventX。如果你暂时不确定可以逐一看一下每个设备支持的事件类型触摸板设备通常会具备EV_ABS、EV_KEY以及BTN_TOUCH、ABS_MT_POSITION_X、ABS_MT_POSITION_Y等能力。3. 圆盘滚轮的工作原理3.1 触摸板事件链路触摸板产生的手势在内核里最终会变成一串输入事件。以多点触摸设备为例内核通常按以下顺序上报一组事件ABS_MT_TRACKING_ID ABS_MT_POSITION_X ABS_MT_POSITION_Y BTN_TOUCH EV_KEY / BTN_LEFT、BTN_RIGHT SYN_REPORTSYN_REPORT表示这一帧事件结束上层程序一般以它作为一批事件的边界。如果我们要自己识别手势也需要依赖这个边界。读事件时最首要的任务就是把同一帧里的MT_POSITION_X、MT_POSITION_Y和BTN_TOUCH拼起来。3.2 圆盘区域坐标模型CF-SV 系列触摸板圆盘的位置是固定的但对驱动来说我们需要拿到它在触摸板原始坐标空间中的位置。触摸板原始坐标范围通常很大比如 X 范围可能是 0 到 3000Y 范围是 0 到 3000。因此圆盘区域可以用一个圆环模型描述圆心坐标(center_x, center_y)内半径inner_radius防止把圆心附近的点击误判为旋转外半径outer_radius超出这个范围说明手指已经离开圆盘判断手指是否在圆盘区域时只需要计算触点与圆心的欧氏距离distance math.hypot(x - center_x, y - center_y) inside inner_radius distance outer_radius这几个参数不能靠猜需要通过调试工具实际采集。后面第 4 章会给出校准步骤。3.3 旋转手势的识别手指在圆盘上滑动的过程中触点相对圆心的角度在持续变化。角度可以通过反三角函数计算angle math.atan2(y - center_y, x - center_x)角度会落在-pi到pi区间。相邻两帧的角度差就是手指转过的角度增量但需要注意角度跨过-pi和pi交界处的情况。例如上一帧角度是 3.1这一帧是 -3.1如果不处理直接相减会得到差值约 -6.2 弧度显然不对。所以在计算增量后需要把角度差归一化到-pi到pidelta angle - last_angle if delta math.pi: delta - 2 * math.pi elif delta -math.pi: delta 2 * math.pi为了屏蔽手指微小抖动我们不应当每变化一个弧度就触发一次滚轮而是设置一个累计阈值。例如设置threshold 0.5弧度当累计角位移超过阈值时输出一个滚轮步进。3.4 uinput 虚拟设备的作用驱动最终输出的是鼠标滚轮事件。最标准的方式是使用内核提供的uinput机制在用户态创建一个虚拟输入设备。上层应用看到这个虚拟设备就像看到了一个真正的滚轮鼠标。我们使用 Pythonevdev库时创建设备的代码非常简洁from evdev import UInput, ecodes capabilities { ecodes.EV_REL: [ecodes.REL_WHEEL, ecodes.REL_HWHEEL], } ui UInput(capabilities, namePanasonic CF-SV Circular Scroll, version0x1)创建完成后写入REL_WHEEL事件并调用syn()同步系统就会把它当作一次滚轮滚动ui.write(ecodes.EV_REL, ecodes.REL_WHEEL, -1) ui.syn()REL_WHEEL为负表示向下滚动为正表示向上滚动。如果想要横向滚动则使用REL_HWHEEL。4. 完整实战案例Python 实现圆盘滚轮驱动下面我们来完整实现一个可运行的驱动守护进程。为了让内容更清晰我把项目拆成几个部分逐步讲解。4.1 创建项目结构建议先创建一个独立目录管理代码、配置和系统服务文件panasonic-cf-scroll/ ├── config.yaml ├── scroll_daemon.py ├── requirements.txt ├── README.md └── systemd/ └── panasonic-cf-scroll.service其中config.yaml存放设备路径、圆心坐标、半径和灵敏度参数。scroll_daemon.py是驱动主程序。requirements.txt记录 Python 依赖。systemd/panasonic-cf-scroll.service用于开机自启。requirements.txt内容如下evdev1.6.1 PyYAML6.0版本号可根据实际环境调整。若下载较慢也可以只写包名不锁定版本。4.2 用 evtest 校准圆盘坐标这一步非常重要。先运行sudo evtest /dev/input/eventX其中/dev/input/eventX换成你的触摸板设备节点。接着在触摸板圆盘上缓慢滑动手指观察终端输出的坐标值。不同机型的圆盘位置和大小不同以我接触过的 CF-SV 系列为例圆盘通常位于触摸板下方居中位置但没有任何数据可以替代实测。校准的思路是记录手指在圆盘最上边缘、最下边缘、最左边缘、最右边缘的坐标。取水平坐标的平均值作为center_x垂直坐标的平均值作为center_y。由边缘点到圆心的距离估算inner_radius和outer_radius。比如采集到圆盘中心大约在(1500, 2200)圆环内半径约120外半径约380可以写入配置device: /dev/input/event4 center_x: 1500 center_y: 2200 inner_radius: 120 outer_radius: 380 scroll_threshold: 0.6 reverse_scroll: falsescroll_threshold的单位是弧度一般 0.4 到 1.0 之间比较适合。设得越小滚动手势越灵敏设得太大需要转很大一圈才会滚动。4.3 编写核心状态机现在编写scroll_daemon.py。先实现一个触摸状态处理类负责跟踪手指是否在圆盘上、角度累计量等状态。#!/usr/bin/env python3 Panasonic CF-SV series circular scroll daemon. import math import sys import time import yaml from evdev import InputDevice, UInput, ecodes class ScrollStateMachine: Track finger angle around a circular scroll wheel. def __init__(self, config): self.config config self.touching False self.last_angle None self.accumulated_delta 0.0 def reset(self): self.touching False self.last_angle None self.accumulated_delta 0.0 def start_touch(self, x, y): self.touching True self.last_angle self._angle_for(x, y) self.accumulated_delta 0.0 def update(self, x, y): if not self.touching: return 0 angle self._angle_for(x, y) if self.last_angle is None: self.last_angle angle return 0 delta angle - self.last_angle if delta math.pi: delta - 2.0 * math.pi elif delta -math.pi: delta 2.0 * math.pi self.last_angle angle self.accumulated_delta delta threshold self.config.get(scroll_threshold, 0.6) steps 0 while self.accumulated_delta threshold: steps 1 self.accumulated_delta - threshold while self.accumulated_delta -threshold: steps - 1 self.accumulated_delta threshold return steps def _angle_for(self, x, y): cx self.config[center_x] cy self.config[center_y] return math.atan2(y - cy, x - cx) def is_in_wheel_area(self, x, y): cx self.config[center_x] cy self.config[center_y] distance math.hypot(x - cx, y - cy) return self.config[inner_radius] distance self.config[outer_radius]这个类核心方法是update()每次传入最新触点坐标后返回需要滚动的步数正数表示向上滚动负数表示向下滚动0 表示不滚动。这里需要注意几点start_touch()在手指触摸圆盘时调用把初始角度记录为last_angle。如果手指离开了圆盘区域但还没有抬起update()仍然可以继续累计角度这样能避免在圆环边界附近因坐标抖动产生跳变。实际项目中如果触摸板上报的是多指触点还需要通过ABS_MT_TRACKING_ID区分是哪根手指。本文示例先以单指场景为主。4.4 主程序读取事件并注入滚轮接下来是主程序部分。需要完成四件事加载配置文件。打开触摸板设备并读取事件。创建 uinput 虚拟滚轮设备。循环处理事件识别手势后注入滚轮。完整代码如下def load_config(path): with open(path, r, encodingutf-8) as f: return yaml.safe_load(f) def main(): config_path sys.argv[1] if len(sys.argv) 1 else config.yaml config load_config(config_path) device_path config[device] if not device_path: print(config.yaml 中 device 不能为空) sys.exit(1) dev InputDevice(device_path) print(fReading events from {dev.name} at {device_path}) # 创建虚拟滚轮设备 ui UInput( { ecodes.EV_REL: [ecodes.REL_WHEEL, ecodes.REL_HWHEEL], }, namePanasonic CF-SV Circular Scroll, version0x1, ) print(uinput virtual wheel device created) state ScrollStateMachine(config) frame {} # 注意grab 会独占触摸板事件避免 libinput 同时处理。 # 在演示阶段可以先注释掉先验证事件读取是否正常。 # dev.grab() try: for event in dev.read_loop(): if event.type ecodes.EV_ABS: if event.code ecodes.ABS_MT_POSITION_X: frame[x] event.value elif event.code ecodes.ABS_MT_POSITION_Y: frame[y] event.value elif event.type ecodes.EV_KEY and event.code ecodes.BTN_TOUCH: if event.value 1: x frame.get(x) y frame.get(y) if x is not None and y is not None: state.start_touch(x, y) elif event.value 0: state.reset() elif event.type ecodes.EV_SYN and event.code ecodes.SYN_REPORT: x frame.get(x) y frame.get(y) if state.touching and x is not None and y is not None: steps state.update(x, y) if steps: direction 1 if config.get(reverse_scroll, False): direction -1 ui.write(ecodes.EV_REL, ecodes.REL_WHEEL, direction * steps) ui.syn() frame {} except KeyboardInterrupt: print(stopped by user) finally: ui.close() dev.close() if __name__ __main__: main()主要逻辑说明用字典frame缓存同一帧事件中的 X/Y 坐标直到收到SYN_REPORT再统一处理。BTN_TOUCH为 1 时以当前坐标作为起始点为 0 时清空状态。注入滚轮时通过ui.write()写入REL_WHEEL再调用ui.syn()让事件立即生效。dev.grab()是可选开关正式使用建议开启调试阶段建议先注释掉避免触摸板被独占后难以排查问题。4.5 运行与验证先确保配置文件中的设备路径正确。如果当前用户没有读取/dev/input/eventX的权限可以临时用sudo运行sudo python3 scroll_daemon.py config.yaml运行时应该输出类似下面的信息Reading events from ELAN0672:00 04F3:31A8 Touchpad at /dev/input/event4 uinput virtual wheel device created此时打开浏览器或终端在圆盘上滑动手指应该能正常滚动页面。如果想确认虚拟事件是否正确产生可以另开一个终端用evtest查看新出现的虚拟设备evtest找到名为Panasonic CF-SV Circular Scroll的设备选择它然后转动圆盘观察输出的EV_REL REL_WHEEL事件Event: time 1620987712.123456, type 2 (EV_REL), code 8 (REL_WHEEL), value 1 Event: time 1620987712.123456, type 0 (EV_SYN), code 0 (SYN_REPORT), value 0如果看到这类事件说明驱动逻辑已经打通。4.6 C 语言版本 uinput 参考有些开发者希望使用 C 语言实现更底层的版本这里给出一段最小可用的 uinput 虚拟设备创建代码方便扩展#include linux/uinput.h #include fcntl.h #include stdio.h #include string.h #include unistd.h int create_virtual_wheel(const char *name) { int fd open(/dev/uinput, O_WRONLY | O_NONBLOCK); if (fd 0) { perror(open /dev/uinput); return -1; } ioctl(fd, UI_SET_EVBIT, EV_REL); ioctl(fd, UI_SET_RELBIT, REL_WHEEL); ioctl(fd, UI_SET_RELBIT, REL_HWHEEL); struct uinput_setup setup; memset(setup, 0, sizeof(setup)); setup.id.bustype BUS_VIRTUAL; setup.id.vendor 0x1234; setup.id.product 0x5678; setup.id.version 1; snprintf(setup.name, UINPUT_MAX_NAME_SIZE, %s, name); ioctl(fd, UI_DEV_SETUP, setup); ioctl(fd, UI_DEV_CREATE); return fd; } void send_wheel(int fd, int value) { struct input_event ev; memset(ev, 0, sizeof(ev)); ev.type EV_REL; ev.code REL_WHEEL; ev.value value; write(fd, ev, sizeof(ev)); memset(ev, 0, sizeof(ev)); ev.type EV_SYN; ev.code SYN_REPORT; ev.value 0; write(fd, ev, sizeof(ev)); }C 版本的好处是性能更好、依赖更少但需要额外处理触摸板事件读取和角度计算整体工作量更大。对大多数用户来说Python 版本已经足够稳定可靠。5. 常见问题与排查思路在实际开发和长期使用过程中最容易遇到的坑主要有以下几个。问题现象常见原因解决思路找不到触摸板设备节点设备在evtest中的名称和预期不一致逐一检查evtest列出的设备查看是否带Touchpad关键字无法打开/dev/input/eventX当前用户缺少 input 组权限使用sudo usermod -aG input $USER并重新登录或临时用 sudo 运行python3提示找不到evdev模块依赖没有安装执行pip3 install evdev检查是否安装到了当前 Python 环境手指在圆盘上滑动没有滚动事件圆心坐标或半径配置明显错误阈值过大用evtest采集边界坐标重新校准把scroll_threshold临时调小到 0.3 测试滚动方向反了手势方向和预期相反在配置文件中设置reverse_scroll: true或修改REL_WHEEL的正负号启动后触摸板鼠标指针失效dev.grab()占用了触摸板事件正式功能中这是预期行为如需调试先注释掉 grab 使用共存模式验证滚动时跳动明显不够平滑单帧坐标噪声大角度累计不稳定增加一阶滤波例如按0.5 * latest 0.5 * previous平滑坐标适当增大阈值虚拟设备创建失败/dev/uinput不存在或权限不足检查内核模块是否加载使用sudo modprobe uinput并确认用户有权限打开设备5.1 关于 grab 独占模式的说明如果你的目标是不影响触摸板鼠标功能直接调用dev.grab()会让触摸板的其他功能全部失效这并不是所有人期望的结果。更稳妥的做法是保持驱动是“跟随模式”读取事件但不 grab让libinput继续处理其他手势。不过在跟随模式下圆盘旋转动作也会被libinput当作普通触摸板操作可能产生意外的指针移动或滚动体验上需要取舍。如果你希望同时保留触摸板指针和圆盘滚动有两种可选方向使用libinput的配置项关闭触摸板自带的“双指滚动”和“边缘滚动”再启用自定义手势但这需要桌面环境配合。将驱动升级为完整的触摸板驱动自行处理指针移动、单击、双指手势和圆盘旋转工作量会明显增加。从工程角度看建议先以外接鼠标或触控板辅助调试验证圆盘手势稳定后再决定是否启用独占模式。5.2 坐标漂移与多指干扰松下触摸板的坐标在触摸瞬间可能存在轻微漂移。如果起始角度误差过大会直接造成第一段滚动方向不确定。解决方法是在start_touch()后先收集前几帧坐标取平均值再初始化角度能有效减少漂移影响。如果用户在圆盘上同时落下两根手指坐标数据会变得混乱。当前这套逻辑只追踪单指因此多指场景下建议丢弃。可以通过ABS_MT_TRACKING_ID的变化来区分触点在检测到第二个有效触点时暂停角度累计。6. 最佳实践与工程建议6.1 把设备路径和坐标做成配置文件不要把设备路径和圆盘坐标硬编码在代码里因为这些数据在不同机型、不同 Linux 发行版下都可能变化。推荐使用 YAML 或 INI 配置文件并在 README 中说明如何通过evtest采集坐标。配置项至少应包含# 触摸板事件设备 device: /dev/input/event4 # 圆盘圆心和半径使用触摸板原始坐标 center_x: 1500 center_y: 2200 inner_radius: 120 outer_radius: 380 # 滚动灵敏度弧度阈值越小越灵敏 scroll_threshold: 0.6 # 是否反转滚动方向 reverse_scroll: false # 是否独占触摸板设备 grab_device: false # 日志级别debug / info / error log_level: info主程序在启动时读取配置修改灵敏度或方向都不需要重新改代码。6.2 使用 systemd 管理守护进程如果希望开机自动运行可以编写一个 systemd service 文件。示例内容如下[Unit] DescriptionPanasonic CF-SV Circular Scroll Daemon Aftermulti-user.target [Service] Typesimple ExecStart/usr/bin/python3 /opt/panasonic-cf-scroll/scroll_daemon.py /etc/panasonic-cf-scroll/config.yaml Restarton-failure RestartSec3 Useryour_user [Install] WantedBymulti-user.target需要根据实际路径和用户名调整。将文件放到/etc/systemd/system/panasonic-cf-scroll.service后执行sudo systemctl daemon-reload sudo systemctl enable panasonic-cf-scroll.service sudo systemctl start panasonic-cf-scroll.service查看日志journalctl -u panasonic-cf-scroll.service -f6.3 调试日志与异常处理驱动长期运行时最怕“静默失败”。建议在关键节点输出日志例如启动时输出触摸板设备名和虚拟设备是否创建成功。配置加载完成后输出圆盘坐标信息。每次注入滚轮事件时在 debug 级别输出步数和方向。可以在 Python 中使用标准logging模块实现而不是到处print。这样通过 systemd 收集日志时格式更统一也方便后续排查。6.4 发布驱动时的注意事项如果你打算把驱动发布给其他 CF-SV 系列用户使用建议做好以下几件事在 README 中明确说明支持的机器型号不要写“支持所有 CF-SV”因为不同批次触摸板坐标可能不同。提供校验脚本或交互式校准向导降低使用门槛。写好许可证文件推荐使用 GPL-3.0 或 MIT避免授权问题。提供安装脚本和卸载脚本方便用户测试后清理。在项目 Issues 中引导用户提供evtest输出而不是直接索要配置文件。6.5 安全与最小权限原则本文方案只读取输入设备事件并向 uinput 写入滚轮事件不会修改系统内核参数也没有删除或覆盖文件的操作。但需要注意只在你自己拥有或获得合法授权的设备上测试。不要在生产环境或重要数据环境中直接运行未经测试的驱动版本。如果使用 sudo 运行确认脚本内容来自可信来源避免执行不知名的一键脚本。7. 总结与后续方向到这里一个可用的松下 CF-SV 系列 Linux 圆盘滚轮驱动已经从原理到代码完整走了一遍。整个方案的关键点可以概括为使用 evdev 读取触摸板原始坐标事件用几何模型判断手指是否处于圆盘区域计算角度变化并累计步进最后通过 uinput 虚拟设备把滚轮事件交给系统。这个思路不局限于松下笔记本凡是触摸板上有类似圆环手势区域的设备都可以用同样的方式适配。接下来值得继续探索的方向有几个感兴趣的读者可以深入研究加入坐标平滑滤波让滚动手感更接近 Windows 下的原生体验。通过ABS_MT_TRACKING_ID跟踪多指触点实现组合手势。把配置校准做成交互式命令行工具动态显示当前触点坐标和推荐圆心。对照libinput的 gesture API尝试在不独占触摸板的前提下与桌面手势共存。圆盘滚轮在 Linux 下没有官方驱动并不代表这个硬件在 Linux 里只能浪费。借助输入子系统提供的用户态接口用 Python 写一个翻译器就能把圆盘手势重新“激活”。如果你手上也有 CF-SV 系列笔记本不妨按照本文的步骤先收集坐标、再调阈值很快就能做出属于自己的 Linux 圆盘滚轮驱动。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进