想要 ssh 到服务器上的特定用户，并使用含 x 的程序，比如说 gitk。 ssh -X user@192.168.0.2 参考： https://blog.csdn.net/fdd096030079/article/details/80986266 https://blog.csdn.net/xxradon/article/details/86636340
字符设备注册 register_chrdev 和 cdev_init + cdev_add 是 Linux 内核中用于注册字符设备的两种不同方法。它们在 使用场景 和 实现方式 上有显著区别。 register_chrdev 特点 一次性注册： 通过一个函数调用完成字符设备的注册。 自动分配主设备号和创建设备节点（如果使用 devfs 或 udev）。 简单易用： 适合简单的字符设备驱动，代码量少，开发速度快。 静态分配： 通常用于静态分配主设备号，无法灵活管理多个次设备号。 旧版接口： 是 Linux 内核早期提供的接口，逐渐被 cdev_init + cdev_add 取代。 使用场景 适用于简单的字符设备驱动。 当设备只需要一个主设备号，且不需要管理多个次设备号时。 适合快速原型开发或学习阶段的驱动程序。 cdev_init + cdev_add 特点 分步注册： cdev_init 初始化字符设备结构体。 cdev_add 将字符设备注册到内核中。 灵活管理： 支持动态分配主设备号和次设备号。 可以管理多个次设备号（如多个设备实例）。 现代接口： 是 Linux 内核推荐的字符设备注册方式，功能更强大，灵活性更高。 与设备模型集成： 可以与内核的设备模型（如 sysfs、udev）集成，支持设备节点的动态创建和管理。 使用场景 适用于复杂的字符设备驱动。 当设备需要动态分配主设备号或管理多个次设备号时。 当需要与内核的设备模型集成（如通过 device_create 创建设备节点）时。 适合生产环境中的驱动程序开发。 主要区别 特性 REGISTER_CHRDEV CDEV_INIT + CDEV_ADD 注册方式 一次性注册 分步注册 设备号管理 静态分配主设备号 动态分配主设备号和次设备号 次设备号支持 不支持管理多个次设备号 支持管理多个次设备号 设备模型集成 不支持内核设备模型（如 sysfs、udev） 支持内核设备模型 灵活性和功能 功能简单，灵活性较低 功能强大，灵活性高 适用场景 简单设备驱动，快速原型开发 复杂设备驱动，生产环境开发 内核版本支持 旧版接口，逐渐被淘汰 现代接口，推荐使用 总结 register_chrdev： 简单易用，适合快速开发简单的字符设备驱动。 功能有限，不支持动态设备号管理和内核设备模型集成。 逐渐被淘汰，不建议在新代码中使用。 cdev_init + cdev_add： 灵活强大，适合开发复杂的字符设备驱动。 支持动态设备号管理、多个次设备号和内核设备模型集成。 是现代 Linux 内核推荐的字符设备注册方式。 在实际开发中，建议优先使用 cdev_init + cdev_add，因为它提供了更强大的功能和更高的灵活性，能够更好地满足现代设备驱动的需求。 创建设备节点 旧方法：mknod， 新方法：device_create。 mknod 函数原型 int mknod(const char *pathname, mode_t mode, dev_t dev); 使用场景 用户空间手动创建设备节点 mknod /dev/my_device c 240 0 缺点 手动管理：需要手动创建设备节点，增加了使用复杂度。 权限问题：需要手动设置权限，可能导致权限不足或安全问题。 缺乏动态性：设备节点无法动态创建或删除，无法适应设备的热插拔。 device_create 函数原型 struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...); 使用场景 在内核驱动中自动创建设备节点。 device_create(my_class, NULL, dev, NULL, "my_device"); 优点 自动管理：设备节点由内核自动创建，无需手动干预。 权限控制：设备节点的权限由内核自动设置，符合安全规范。 动态支持：支持设备的热插拔，设备节点可以动态创建或删除。 集成设备模型：与内核的设备模型（如 sysfs、udev）集成，提供更强大的功能。 为什么要使用新函数？ 自动化和动态化 新方法通过内核自动创建设备节点，支持设备的热插拔和动态管理，减少了手动操作的复杂性。 权限和安全性 新方法由内核自动设置设备节点的权限，避免了手动设置权限可能带来的安全问题。 集成设备模型 新方法与内核的设备模型（如 sysfs、udev）紧密集成，提供了更强大的设备管理功能（如属性文件、事件通知等）。 一致性和可维护性 新方法符合现代 Linux 内核的设计理念，代码更加一致和可维护。 总结 旧方法：mknod 用于在用户空间手动创建设备节点，缺乏自动化和动态支持。 新方法：device_create 用于在内核中自动创建设备节点，支持动态管理、权限控制和设备模型集成。 推荐使用新方法：新方法符合现代 Linux 内核的设计理念，提供了更强大的功能和更好的可维护性。 class_create 和 device_create class_create 和 device_create 是 Linux 内核中用于 设备模型管理 的函数，它们分别用于 创建设备类 和 创建设备节点。 class_create 作用 创建一个设备类（struct class），用于管理一组具有相同类型的设备。 设备类在 /sys/class/ 目录下创建一个目录，用于存放设备的属性和状态信息。 使用场景 当你需要为一组设备（如多个字符设备）创建一个共同的类别时，使用 class_create。 例如，为所有 I2C 设备创建一个 i2c-dev 类，为所有 GPIO 设备创建一个 gpio 类。 示例 struct class *my_class; my_class = class_create(THIS_MODULE, "my_device_class"); device_create 作用 在 /dev 目录下创建设备节点，并在 /sys/class/ 目录下创建设备的属性文件。 设备节点是用户空间与内核设备交互的接口。 使用场景 当你需要在 /dev 目录下创建设备节点时，使用 device_create。 例如，在字符设备驱动中，为每个设备创建设备节点（如 /dev/my_device）。 示例 device_create(my_class, NULL, dev, NULL, "my_device"); 典型的使用流程 my_class = class_create(THIS_MODULE, CLASS_NAME); device_create(my_class, NULL, dev, NULL, DEVICE_NAME); 总结 class_create： 创建设备类，用于管理一组具有相同类型的设备。 使用场景：为多个设备创建一个共同的类别。 device_create： 创建设备节点，用于用户空间与内核设备交互。 使用场景：在 /dev 目录下创建设备节点。 cdev_add 和 device_add 关系 在 Linux 内核中，cdev_add 和 device_add 是两个不同的函数，分别用于 字符设备注册 和 设备模型注册。如果只使用 cdev_add 而没有使用 device_add，会有以下缺点和问题： 缺少设备模型支持 device_add 是 Linux 设备模型的一部分，用于将设备注册到内核的设备树中。如果只使用 cdev_add，设备不会被添加到内核的设备树中，导致以下问题： 无法通过 sysfs 管理设备 sysfs 是内核提供的文件系统，用于管理设备和驱动的属性。 如果设备没有通过 device_add 注册，sysfs 中不会出现该设备的目录，用户无法通过 sysfs 查看或修改设备属性。 无法自动加载驱动 内核的设备模型支持自动加载驱动（通过 MODALIAS 机制）。 如果设备没有通过 device_add 注册，内核无法自动加载与该设备匹配的驱动。 无法使用 udev 管理设备 udev 是用户空间的设备管理工具，依赖于内核的设备模型。 如果设备没有通过 device_add 注册，udev 无法管理该设备（如创建设备节点、设置权限等）。 缺少设备节点 device_create 是 device_add 的一部分，用于在 /dev 目录下创建设备节点。如果只使用 cdev_add，设备节点不会被自动创建，导致以下问题： 需要手动创建设备节点 必须手动使用 mknod 命令创建设备节点，增加了使用复杂度。 mknod /dev/my_device c 240 0 设备节点权限问题 手动创建设备节点时，需要手动设置权限，可能导致权限不足或安全问题。 chmod 666 /dev/my_device 缺少设备的热插拔支持 device_add 支持设备的热插拔功能。如果只使用 cdev_add，设备无法支持热插拔，导致以下问题： 无法动态加载和卸载设备 设备无法在运行时动态加载或卸载。 例如，无法通过 echo 命令将设备从内核中移除。 无法与用户空间交互 设备的热插拔事件无法通知用户空间（如通过 udev）。 缺少设备的状态管理 device_add 会将设备的状态（如 probe、remove 等）记录到内核中。如果只使用 cdev_add，设备的状态无法被内核管理，导致以下问题： 无法跟踪设备状态 内核无法知道设备是否已被初始化或移除。 例如，无法通过 sysfs 查看设备的状态。 无法处理设备的依赖关系 内核无法处理设备与其他设备或资源的依赖关系。 例如，无法确保设备在依赖资源可用后再初始化。 总结 如果只使用 cdev_add，设备无法被注册到内核的设备树中，导致无法通过 sysfs 管理设备、无法自动加载驱动、无法使用 udev 管理设备等问题。 为了充分利用内核的设备模型和功能，建议同时使用 cdev_add 和 device_add。 gpio, gpiod gpiod 和 gpio 是 Linux 内核中用于管理 GPIO（通用输入输出）的两种不同接口。它们的主要区别在于 实现方式 和 使用场景。以下是它们的详细对比 gpio 特点 旧版接口： 是 Linux 内核早期提供的 GPIO 管理接口。 基于 sysfs 文件系统，通过 /sys/class/gpio 目录访问 GPIO。 用户空间接口： 主要通过文件操作（如 open、read、write）控制 GPIO。 例如，通过 echo 命令导出 GPIO 或设置 GPIO 方向。 简单易用： 适合简单的 GPIO 操作，无需编写内核代码。 使用场景 在用户空间快速测试 GPIO 功能。 简单的嵌入式系统或原型开发。 不需要复杂 GPIO 管理的场景。 gpiod 接口 特点 新版接口： 是 Linux 内核推荐的 GPIO 管理接口。 基于 libgpiod 库，提供更强大和灵活的功能。 内核空间和用户空间接口： 支持在内核空间和用户空间操作 GPIO。 提供丰富的 API，支持复杂的 GPIO 管理（如中断、多 GPIO 操作）。 设备树支持： 与设备树（Device Tree）紧密集成，支持动态配置 GPIO。 性能优化： 比 gpio 接口更高效，适合高性能和实时性要求高的场景。 使用场景 复杂的嵌入式系统或生产环境。 需要高性能和实时性支持的场景。 需要与设备树集成的场景。 需要复杂 GPIO 管理（如中断、多 GPIO 操作）的场景。 主要区别 特性 GPIO 接口 GPIOD 接口 接口类型 旧版接口，基于 sysfs 新版接口，基于 libgpiod 使用场景 用户空间，简单 GPIO 操作 内核空间和用户空间，复杂 GPIO 操作 性能 较低 较高 功能 功能有限 功能强大（支持中断、多 GPIO 操作等） 设备树支持 不支持 支持 推荐使用 逐渐被淘汰 推荐使用 总结 gpio 接口： 是旧版接口，基于 sysfs，适合简单的 GPIO 操作。 逐渐被淘汰，不建议在新代码中使用。 gpiod 接口： 是新版接口，基于 libgpiod，功能强大，性能优越。 支持复杂的 GPIO 管理和设备树集成，推荐在新项目中使用。 在实际开发中，建议优先使用 gpiod 接口，因为它提供了更强大的功能和更好的性能，能够满足现代嵌入式系统的需求。 devm 和 旧版方式 devm（Device Resource Management）是 Linux 内核中用于 自动管理设备资源 的一套接口。它通过将资源的生命周期与设备绑定，确保在设备卸载时自动释放资源，从而减少资源泄漏的风险。devm 接口是 新版接口，并没有所谓的“旧版接口”，但它与传统的资源管理方式（手动管理）有显著区别。 传统资源管理方式（手动管理） 特点 手动分配和释放资源： 开发者需要显式调用资源分配函数（如 kmalloc、request_irq 等）。 在设备卸载时，需要显式调用资源释放函数（如 kfree、free_irq 等）。 容易出错： 如果忘记释放资源，会导致资源泄漏。 如果多次释放资源，会导致内核崩溃。 示例 static int my_probe(struct platform_device *pdev) { void *buffer; int irq; // 分配内存 buffer = kmalloc(1024, GFP_KERNEL); if (!buffer) return -ENOMEM; // 注册中断 irq = request_irq(IRQ_NUM, my_handler, 0, "my_device", NULL); if (irq < 0) { kfree(buffer); return irq; } // 在设备卸载时，需要手动释放资源 return 0; } static int my_remove(struct platform_device *pdev) { // 释放中断 free_irq(IRQ_NUM, NULL); // 释放内存 kfree(buffer); return 0; } devm 接口（自动管理资源） 特点 自动释放资源： 资源与设备绑定，当设备卸载时，内核自动释放资源。 开发者无需显式调用资源释放函数。 减少错误： 避免资源泄漏和重复释放的问题。 简化驱动代码，提高开发效率。 示例 static int my_probe(struct platform_device *pdev) { void *buffer; int irq; // 分配内存（自动释放） buffer = devm_kzalloc(&pdev->dev, 1024, GFP_KERNEL); if (!buffer) return -ENOMEM; // 注册中断（自动释放） irq = devm_request_irq(&pdev->dev, IRQ_NUM, my_handler, 0, "my_device", NULL); if (irq < 0) return irq; // 无需手动释放资源 return 0; } static int my_remove(struct platform_device *pdev) { // 无需手动释放资源 return 0; } 主要区别 特性 传统方式（手动管理） DEVM 接口（自动管理） 资源释放 需要手动释放资源 内核自动释放资源 错误风险 容易导致资源泄漏或重复释放 减少资源泄漏和重复释放的风险 代码复杂度 代码复杂度较高，需要显式管理资源 代码简化，资源管理与设备绑定 适用场景 适用于所有场景，但需要开发者小心管理资源 推荐在新代码中使用，减少开发负担 devm 接口的常见函数 devm 接口提供了一系列资源管理函数，以下是常见的函数： 函数 说明 devm_kzalloc 分配内存，设备卸载时自动释放。 devm_kmalloc 分配内存，设备卸载时自动释放。 devm_request_irq 注册中断，设备卸载时自动释放。 devm_ioremap 映射 I/O 内存，设备卸载时自动释放。 devm_gpiod_get 获取 GPIO 描述符，设备卸载时自动释放。 devm_clk_get 获取时钟资源，设备卸载时自动释放。 devm_regulator_get 获取电源调节器，设备卸载时自动释放。 总结 传统方式： 需要手动分配和释放资源，容易出错，代码复杂度高。 devm 接口： 资源与设备绑定，内核自动释放资源，减少错误，简化代码。 推荐使用 devm 接口： 在新代码中优先使用 devm 接口，以减少资源管理的工作量和错误风险。
wifi 透传模块 hf-lpt220 常用命令，也就是 AT 命令： [...]
input 子系统介绍 参考资料 Linux 5.x内核文档 Documentation\input\input-programming.rst Documentation\input\event-codes.rst Linux 4.x内核文档 Documentation\input\input-programming.txt Documentation\input\event-codes.txt input 种类 按键：EV_KEY，比如键盘 相对位移：EV_REL，比如鼠标 绝对位移：EV_ABS，比如触摸屏 杂项：EV_MSC 软件：EV_SW LED：EV_LED 声音：EV_SND 会自动发出重复按键：EV_REP 电源开关、按键：EV_PWR 驱动框架 input device, 主要了解的是 gpio 和 uinput. uinput 主要用于用户态的键盘输入模拟用。 app 输入系统框架及调试 输入系统框架 数据的流程如下： APP发起读操作，若无数据则休眠； 用户操作设备，硬件上产生中断； 输入系统驱动层对应的驱动程序处理中断：读取到数据，转换为标准的输入事件，向核心层汇报。所谓输入事件就是一个“struct input_event”结构体。 核心层可以决定把输入事件转发给上面哪个handler来处理： 从handler的名字来看，它就是用来处输入操作的。有多种handler，比如：evdev_handler、kbd_handler、joydev_handler等等。 最常用的是evdev_handler：它只是把input_event结构体保存在内核buffer等，APP来读取时就原原本本地返回。它支持多个APP同时访问输入设备，每个APP都可以获得同一份输入事件。 当APP正在等待数据时，evdev_handler会把它唤醒，这样APP就可以返回数据。 APP对输入事件的处理：APP获得数据的方法有2种：直接访问设备节点(比如/dev/input/event0,1,2,...)，或者通过tslib、libinput这类库来间接访问设备节点。这些库简化了对数据的处理。 调试 hexdump /dev/input/event0 命令行获取输入设备当前的数据。点击触摸屏就可以看到数据刷新。 ls /dev/input/* -l 查看输入设备节点 cat /proc/bus/input/devices 查看详细的设备信息 详细设备信息解析 主要是 include\linux\input.h 中 struct input_dev 里面的值的表示。 struct input_dev { const char *name; const char *phys; const char *uniq; struct input_id id; unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)]; unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)]; unsigned long ledbit[BITS_TO_LONGS(LED_CNT)]; unsigned long sndbit[BITS_TO_LONGS(SND_CNT)]; unsigned long ffbit[BITS_TO_LONGS(FF_CNT)]; unsigned long swbit[BITS_TO_LONGS(SW_CNT)]; ... I:id of the device(设备ID) 该参数由结构体struct input_id来进行描述，驱动程序中会定义这样的结构体： // include\uapi\linux\input.h struct input_id { __u16 bustype; __u16 vendor; __u16 product; __u16 version; }; N:name of the device 设备名称 P:physical path to the device in the system hierarchy 系统层次结构中设备的物理路径。 S:sysfs path 位于sys文件系统的路径 U:unique identification code for the device(if device has it) 设备的唯一标识码 H:list of input handles associated with the device. 与设备关联的输入句柄列表。 B:bitmaps(位图) PROP:device properties and quirks(设备属性) EV:types of events supported by the device(设备支持的事件类型) KEY:keys/buttons this device has(此设备具有的键/按钮) MSC:miscellaneous events supported by the device(设备支持的其他事件) LED:leds present on the device(设备上的指示灯) B位图解析 “B: EV=b”用来表示该设备支持哪类输入事件。b的二进制是1011，bit0、1、3为1，表示该设备支持0、1、3这三类事件，即EV_SYN、EV_KEY、EV_ABS。 “B: ABS=2658000 3” : 表示该设备支持 EV_ABS 这一类事件中的哪一些事件。这是2个32位的数字：0x2658000、0x3，高位在前低位在后，组成一个64位的数字：“0x2658000,00000003”，数值为1的位有：0、1、47、48、50、53、54，即：0、1、0x2f、0x30、0x32、0x35、0x36，对应以下这些宏： // include\uapi\linux\input-event-codes.h #define ABS_X 0x00 #define ABS_Y 0x01 ... #define ABS_MT_SLOT 0x2f /* MT slot being modified */ #define ABS_MT_TOUCH_MAJOR 0x30 /* Major axis of touching ellipse */ #define ABS_MT_TOUCH_MINOR 0x31 /* Minor axis (omit if circular) */ #define ABS_MT_WIDTH_MAJOR 0x32 /* Major axis of approaching ellipse */ #define ABS_MT_WIDTH_MINOR 0x33 /* Minor axis (omit if circular) */ #define ABS_MT_ORIENTATION 0x34 /* Ellipse orientation */ #define ABS_MT_POSITION_X 0x35 /* Center X touch position */ #define ABS_MT_POSITION_Y 0x36 /* Center Y touch position */ 即这款输入设备支持上述的ABS_X、ABS_Y、ABS_MT_SLOT、ABS_MT_TOUCH_MAJOR、ABS_MT_WIDTH_MAJOR、ABS_MT_POSITION_X、ABS_MT_POSITION_Y这些绝对位置事件(它们的含义在后面讲解电容屏时再细讲)。 数据 include\uapi\linux\input.h 中的 struct input_event 就是传递的数据。 struct input_event { struct timeval time; __u16 type; __u16 code; __s32 value; }; type include\uapi\linux\input-event-codes.h 中有 event type， 常用的就三种： #define EV_SYN 0x00 #define EV_KEY 0x01 #define EV_REL 0x02 #define EV_ABS 0x03 code ： KEY_A, ABS_X 等 #define KEY_A 30 ... #define ABS_X 0x00 value: 0 松开，1 按下，2 长按 当本次的所有数据全部上传完成，就需要最后通过 EV_SYN 这个事件来说明。比如下面的数据中，通过 EV_SYN 来表明两次按键操作。 app 现场编程读取输入 evdev_do_ioctl 获取输入设备相关信息，需要使用 ioctrl, 对应的驱动在 drivers\input\evdev.c 中的 static long evdev_do_ioctl(struct file *file, unsigned int cmd, void __user *p, int compat_mode) evdev_do_ioctl 中，会对 cmd 进行判断，输入的 cmd 会用到有关宏: // drivers\input\evdev.c static long evdev_do_ioctl(struct file *file, unsigned int cmd, void __user *p, int compat_mode) { ... /* First we check for fixed-length commands */ switch (cmd) { case EVIOCGVERSION: return put_user(EV_VERSION, ip); case EVIOCGID: if (copy_to_user(p, &dev->id, sizeof(struct input_id))) return -EFAULT; ... // include\uapi\linux\input.h #define EVIOCGID _IOR('E', 0x02, struct input_id) /* get device ID */ // include\uapi\asm-generic\ioctl.h #define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size))) #define _IOC(dir,type,nr,size) \ (((dir) << _IOC_DIRSHIFT) | \ // bit 29 ((type) << _IOC_TYPESHIFT) | \ // bit 8 ((nr) << _IOC_NRSHIFT) | \ // bit 0 ((size) << _IOC_SIZESHIFT)) // bit 16 要读取输入设备的evbit时，ioctl的request要写为 EVIOCGBIT(0, size)，size的大小可以由你决定：你想读多少字节就设置为多少。这个宏的定义如下： #define EVIOCGBIT(ev,len) _IOC(_IOC_READ, 'E', 0x20 + (ev), len) /* get event bits */ 使用 fd = open(argv[1], O_RDWR); err = ioctl(fd, EVIOCGID, &id); len = ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit); app 查询 休眠唤醒 fd = open(argv[1], O_RDWR | O_NONBLOCK); len = read(fd, &event, sizeof(event)); app poll select 读取 struct pollfd fds[1]; nfds_t nfds = 1; fd = open(argv[1], O_RDWR | O_NONBLOCK); while (1) { fds[0].fd = fd; fds[0].events = POLLIN; fds[0].revents = 0; ret = poll(fds, nfds, 5000); if (ret > 0) { if (fds[0].revents == POLLIN) { while (read(fd, &event, sizeof(event)) == sizeof(event)) { printf("get event: type = 0x%x, code = 0x%x, value = 0x%x\n", event.type, event.code, event.value); } } } else if (ret == 0) { printf("time out\n"); } else { printf("poll err\n"); } } int nfds; struct timeval tv; fd_set readfds; while (1) { /* 设置超时时间 */ tv.tv_sec = 5; tv.tv_usec = 0; /* 想监测哪些文件? */ FD_ZERO(&readfds); /* 先全部清零 */ FD_SET(fd, &readfds); /* 想监测fd */ /* 函数原型为: int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); * 我们为了"read"而监测, 所以只需要提供readfds */ nfds = fd + 1; /* nfds 是最大的文件句柄+1, 注意: 不是文件个数, 这与poll不一样 */ ret = select(nfds, &readfds, NULL, NULL, &tv); if (ret > 0) /* 有文件可以提供数据了 */ { /* 再次确认fd有数据 */ if (FD_ISSET(fd, &readfds)) { while (read(fd, &event, sizeof(event)) == sizeof(event)) { printf("get event: type = 0x%x, code = 0x%x, value = 0x%x\n", event.type, event.code, event.value); } } } else if (ret == 0) /* 超时 */ { printf("time out\n"); } else /* -1: error */ { printf("select err\n"); } } app 异步读取数据 int fd; void my_sig_handler(int sig) { struct input_event event; while (read(fd, &event, sizeof(event)) == sizeof(event)) { printf("get event: type = 0x%x, code = 0x%x, value = 0x%x\n", event.type, event.code, event.value); } } /* 注册信号处理函数 */ signal(SIGIO, my_sig_handler); /* 打开驱动程序 */ fd = open(argv[1], O_RDWR | O_NONBLOCK); /* 把APP的进程号告诉驱动程序 */ fcntl(fd, F_SETOWN, getpid()); /* 使能"异步通知" */ flags = fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, flags | FASYNC); while (1) { printf("main loop count = %d\n", count++); sleep(2); } 电阻屏和电容屏 电阻屏 // 按下时： EV_KEY BTN_TOUCH 1 /* 按下 */ EV_ABS ABS_PRESSURE 1 /* 压力值，可以上报，也可以不报，可以是其他压力值 */ EV_ABS ABS_X x_value /* X坐标 */ EV_ABS ABS_Y y_value /* Y坐标 */ EV_SYNC 0 0 /* 同步事件 */ // 松开时： EV_KEY BTN_TOUCH 0 /* 松开 */ EV_ABS ABS_PRESSURE 0 /* 压力值，可以上报，也可以不报 */ EV_SYNC 0 0 /* 同步事件 */ 电容屏 参考文档：Linux内核Documentation\input\multi-touch-protocol.rst。 // 当有2个触点时(type, code, value)： EV_ABS ABS_MT_SLOT 0 // 这表示“我要上报一个触点信息了”，用来分隔触点信息 EV_ABS ABS_MT_TRACKING_ID 45 // 这个触点的ID是45 EV_ABS ABS_MT_POSITION_X x[0] // 触点X坐标 EV_ABS ABS_MT_POSITION_Y y[0] // 触点Y坐标 EV_ABS ABS_MT_SLOT 1 // 这表示“我要上报一个触点信息了”，用来分隔触点信息 EV_ABS ABS_MT_TRACKING_ID 46 // 这个触点的ID是46 EV_ABS ABS_MT_POSITION_X x[1] // 触点X坐标 EV_ABS ABS_MT_POSITION_Y y[1] // 触点Y坐标 EV_SYNC SYN_REPORT 0 // 全部数据上报完毕 // 当ID为45的触点正在移动时 EV_ABS ABS_MT_SLOT 0 // 这表示“我要上报一个触点信息了”，之前上报过ID，就不用再上报ID了 EV_ABS ABS_MT_POSITION_X x[0] // 触点X坐标 EV_SYNC SYN_REPORT 0 // 全部数据上报完毕 // 松开ID为45的触点时(在前面slot已经被设置为0，这里这需要再重新设置slot，slot就像一个全局变量一样：如果它没变化的话，就无需再次设置)： // 刚刚设置了ABS_MT_SLOT为0，它对应ID为45，这里设置ID为-1就表示ID为45的触点被松开了 EV_ABS ABS_MT_TRACKING_ID -1 EV_SYNC SYN_REPORT 0 // 全部数据上报完毕 // 最后，松开ID为46的触点： EV_ABS ABS_MT_SLOT 1 // 这表示“我要上报一个触点信息了”，在前面设置过slot 1的ID为46 EV_ABS ABS_MT_TRACKING_ID -1 // ID为-1，表示slot 1被松开，即ID为46的触点被松开 EV_SYNC SYN_REPORT // 全部数据上报完毕 调试数据 hexdump /dev/input/event0 一个手指 电容屏的实际触摸数据，为了兼容以前，后面还按照电阻屏的模式增加了电阻屏数据。 只有一个手指的时候，没有 EV_ABS ABS_MT_SLOT 数据 触点是椭圆形，长轴是 ABS_MT_TOUCH_MAJOR，短轴是 ABS_MT_TOUCH_MINOR 手指也是椭圆形，长轴是 ABS_MT_WIDTH_MAJOR，短轴是 ABS_MT_WIDTH_MINOR 多个手指 app tslib 框架 框架 源码 ts_dev 结构体。 ts_setup ts_open 获取触摸屏设备从前到后: 检查参数中的设备名 dev_name 检查环境变量 getenv("TSLIB_TSDEVICE") 三个默认设备节点: static const char * const ts_name_default[] = { "/dev/input/ts", "/dev/input/touchscreen", "/dev/touchscreen/ucb1x00", NULL }; scan_devices() 扫描 /dev/input 下的设备节点。 获取了触摸屏设备之后，就 ts_open. ts_config 配置文件的选择从前到后: 环境变量: getenv("TSLIB_CONFFILE) 预先设置的配置文件: TS_CONF 正常就是 /etc/ts.conf, 主要内容如下: module_raw input module pthres pmin=1 module dejitter delta=100 module linear ts_read, ts_read_mt 通过链表中的 next 指针，调用前级 module. app ts_lib 交叉编译 配置编译工具链 export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf- export PATH=$PATH:/home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/bin 交叉编译tslib ./configure --host=arm-linux-gnueabihf --prefix=/ make make install DESTDIR=$PWD/tmp 查看工具链中头文件、库文件目录 echo 'main(){}'| arm-linux-gnueabihf-gcc -E -v - 把头文件、库文件放到工具链目录下 cd tslib-1.21/tmp/ cp include/* /home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/bin/../arm-linux-gnueabihf/libc/usr/include cp -d lib/*so* /home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/bin/../arm-linux-gnueabihf/libc/usr/lib/ 测试tslib cp /mnt/tslib-1.21/tmp/lib/* -drf /lib cp /mnt/tslib-1.21/tmp/bin/* /bin cp /mnt/tslib-1.21/tmp/etc/ts.conf -d /etc mv /etc/init.d/S07hmi /root reboot ts_test_mt app 基于 tslib 的测试程序 直接读取 /dev/input/event0 的数据需要自己处理，使用 tslib 就会非常简单，调用 ts_read_mt 这个函数就可以一次拿到所有的触摸数据。 int ts_read_mt(struct tsdev *ts, struct ts_sample_mt **samp, int max_slots, int nr); 中，nr 表示要读几次数据，max_slots 表示每次读几个点的数据，ts_sample_mt 这个是数据格式。触摸过程中，可能有手指的变化，这种情况，在 ts_sample_mt 中通过 short valid 来表示。 整个程序的流程: ts = ts_setup(NULL, 0); 打开触摸 ioctl 获取 max_slots ioctl(ts_fd(ts), EVIOCGABS(ABS_MT_SLOT), &slot); max_slots = slot.maximum + 1 - slot.minimum; ret = ts_read_mt(ts, samp_mt, max_slots, 1); 获取触摸数据。 input 子系统框架 参考资料： Linux 5.x内核文档 Documentation\input\input-programming.rst Documentation\input\event-codes.rst Linux 4.x内核文档 Documentation\input\input-programming.txt Documentation\input\event-codes.txt 普通字符设备框架 确定主设备号 创建file_operations结构体, 填充drv_open/drv_read/drv_ioctl等函数 注册file_operations结构体 register_chrdev(major, &fops, name), major = 0, 自动生成 major 入口函数, 出口函数 辅助函数(帮助系统自动创建设备节点) class_create device_create input 框架 内部实现 也是分左右两边的讨论，左边是资源，右边是处理。 左边使用 int __must_check input_register_device(struct input_dev *); 来注册。 右边使用 int __must_check input_register_handler(struct input_handler *); 来注册。 左边设备资源 input_dev 右边处理 input_handler 处理的时候，也是先试试 match，不行的话，再 id_table。 匹配上之后，用 connect 来连接。 connect 在 drivers\input\evdev.c 中的 static int evdev_connect(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id). error = input_register_handle(&evdev->handle); 把 input_dev 和 input_handler 保存到 handle 中，并注册. 而且还会把这个 handle 分别放到 input_dev 和 input_handler 各自结构体的链表 h_list 中。 cdev_init(&evdev->cdev, &evdev_fops); error = cdev_add(&evdev->cdev, evdev->dev.devt, 1); h_list 链表，表明一个 input_dev 可以有多个 input_handler 来进行处理，一个 input_handler 也可以有多个 input_dev 需要处理。 资源和处理的关系 input_handle 设备获取、上报数据：input_event struct input_event { struct timeval time; __u16 type; __u16 code; __s32 value; }; 有 input_event 之后，需要先用 input_handler->filter 进行过滤，如果通过，再用 input_handler->events 来处理，实在不行，才用 input_handler->event 来处理。 注册流程 drivers\input\input.c 中的 int input_register_device(struct input_dev *dev) list_add_tail(&dev->node, &input_dev_list); 把节点加入到链表中。 list_for_each_entry(handler, &input_handler_list, node) input_attach_handler(dev, handler); 遍历所有 handler，然后尝试 match, 再 connect. input_handler_list 中含有 evdev_handler, 因为 evdev 在 init 的时候，调用了 input_register_handler(&evdev_handler); 把自己注册到了 input_handler_list 中。 所以，才能在 input_register_device 是，通过 connect 来调用 evdev 的 evdev_connect. 在 evdev_connect 中，先分配evdev = kzalloc(sizeof(struct evdev), GFP_KERNEL);, 再设置 evdev->handle, 然后再 input_register_handle 注册 // device evdev->handle.dev = input_get_device(dev); // handler evdev->handle.handler = handler; input_register_handle 中，把 handle 分别加入 device 和 handler 的链表中。 if (handler->filter) list_add_rcu(&handle->d_node, &dev->h_list); else list_add_tail_rcu(&handle->d_node, &dev->h_list); mutex_unlock(&dev->mutex); list_add_tail_rcu(&handle->h_node, &handler->h_list); cdev_init(&evdev->cdev, &evdev_fops);, error = cdev_add(&evdev->cdev, evdev->dev.devt, 1); 注册字符设备。 error = device_add(&evdev->dev); 增加到设备管理中，加入 /dev, /sysfs 中，与 udev 更加紧密。 app 使用流程 open open("/dev/input/event0"), 首先会调用 evdev 中的 open, static int evdev_open(struct inode *inode, struct file *file) evdev_open 中，先创建 app 对应的 client, struct evdev_client *client;; 然后再 evdev_attach_client(evdev, client); 把 client 加入到列表中 list_add_tail_rcu(&client->node, &evdev->client_list);; 这样同一个输入设备，可以让多个 app 来使用。 static void evdev_attach_client(struct evdev *evdev, struct evdev_client *client), 在 device 层，使用 input_dev 表示一个输入设备；在 handler 层，使用 evdev 来表示一个输入设备。 read 使用 evdev 的 read, static ssize_t evdev_read(struct file *file, char __user *buffer, size_t count, loff_t *ppos) 会在 evdev->wait 上休眠。 error = wait_event_interruptible(evdev->wait, client->packet_head != client->tail || !evdev->exist || client->revoked); 当设备按下之后，会产生中断，并调用 input_event 来上报，void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value) input_event 调用 input_handle_event(dev, type, code, value); 再调用 input_pass_values(dev, dev->vals, dev->num_vals); 在 static void input_pass_values(struct input_dev *dev, struct input_value *vals, unsigned int count) 中 遍历并调用 handler: list_for_each_entry_rcu(handle, &dev->h_list, d_node) if (handle->open) { count = input_to_handler(handle, vals, count); if (!count) break; } static unsigned int input_to_handler(struct input_handle *handle, struct input_value *vals, unsigned int count) 会先调用 filter，然后再 events, 再 event. if (handler->filter) ... if (handler->events) handler->events(handle, vals, count); else if (handler->event) for (v = vals; v != vals + count; v++) handler->event(handle, v->type, v->code, v->value); static void evdev_events(struct input_handle *handle, const struct input_value *vals, unsigned int count) 中，会 遍历所有的 client 列表，然后传数据 evdev_pass_values. list_for_each_entry_rcu(client, &evdev->client_list, node) evdev_pass_values(client, vals, count, ev_time); static void evdev_pass_values(struct evdev_client *client, const struct input_value *vals, unsigned int count, ktime_t *ev_time) 中，先传数据 __pass_event(client, &event); 然后再 wake_up_interruptible(&evdev->wait); 唤醒 client 也就是 app. static void __pass_event(struct evdev_client *client, const struct input_event *event) 中，把数据都填充到 client 的 buffer 中。 ... client->buffer[client->tail].time = event->time; client->buffer[client->tail].type = EV_SYN; client->buffer[client->tail].code = SYN_DROPPED; client->buffer[client->tail].value = 0; ... 总结 虽然 input 有点绕，但是 input_handler 正常可以直接用 evdev 的，我们一般只需要去实现 input_dev 即可。 编写 input_dev 框架 想要实现 input_dev，有两种方法: 直接写一个驱动，在里面实现分配，设置，注册 input_dev. 但是这样就写死了。 使用 platform。 使用设备树, 设备树会被转化为 platform_device 通过设备树，然后加载 对应的 platform_driver 在这个 platform_driver 的 probe 中，去实现 分配，设置，注册 input_dev. 总体而言，肯定使用 platform 的方法更好。 platform device / { input_dev_demo { compatible = "100ask,input_dev_demo"; interrupts = <...>; }; }; platform driver 参考 drivers\input\keyboard\gpio_keys.c 去写。 主要做的就是 alloc, set, register 的工作。 基础框架 static const struct of_device_id input_dev_demo_of_match[] = { { .compatible = "100ask,input_dev_demo", }, { }, }; static struct platform_driver input_dev_demo_driver = { .probe = input_dev_demo_probe, .remove = input_dev_demo_remove, .driver = { .name = "input_dev_demo", .of_match_table = input_dev_demo_of_match, } }; static int __init input_dev_demo_init(void) { return platform_driver_register(&input_dev_demo_driver); } static void __exit input_dev_demo_exit(void) { platform_driver_unregister(&input_dev_demo_driver); } module_init(input_dev_demo_init); module_exit(input_dev_demo_exit); MODULE_LICENSE("GPL"); input_dev 相关 /* alloc/set/register input_dev */ g_input_dev = devm_input_allocate_device(dev); g_input_dev->name = "input_dev_demo"; g_input_dev->phys = "input_dev_demo"; g_input_dev->dev.parent = dev; g_input_dev->id.bustype = BUS_HOST; g_input_dev->id.vendor = 0x0001; g_input_dev->id.product = 0x0001; g_input_dev->id.version = 0x0100; /* set 1: which type event ? */ __set_bit(EV_KEY, g_input_dev->evbit); __set_bit(EV_ABS, g_input_dev->evbit); /* set 2: which event ? */ __set_bit(BTN_TOUCH, g_input_dev->keybit); __set_bit(ABS_MT_SLOT, g_input_dev->absbit); __set_bit(ABS_MT_POSITION_X, g_input_dev->absbit); __set_bit(ABS_MT_POSITION_Y, g_input_dev->absbit); /* set 3: event params ? */ input_set_abs_params(g_input_dev, ABS_MT_POSITION_X, 0, 0xffff, 0, 0); input_set_abs_params(g_input_dev, ABS_MT_POSITION_Y, 0, 0xffff, 0, 0); error = input_register_device(g_input_dev); 中断相关 static int g_irq; static irqreturn_t input_dev_demo_isr(int irq, void *dev_id) { /* read data */ /* report data */ input_event(g_input_dev, EV_KEY, XX, 0); input_sync(g_input_dev); return IRQ_HANDLED; } /* hardware opration */ irq = platform_get_resource(pdev, IORESOURCE_IRQ, 0); g_irq = irq->start; request_irq(irq->start, input_dev_demo_isr, IRQF_TRIGGER_RISING, "input_dev_demo_irq", NULL); 简单触摸驱动，qemu 设备树 / { input_dev_demo { compatible = "100ask,input_dev_demo"; reg = <0x021B4000 16>; //interrupt-parent = <&gpio1>; //interrupts = <5 IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING>; gpios = <&gpio1 5 1>; }; }; 因为用到的触摸寄存器是从 0x021B4000 开始的 4个寄存器，每个寄存器 32位，所以 reg = <0x021B4000 16>; interrupt 直接参考其他的设备树文件去写即可。 正常中断直接写 interrupts 即可，但是这个 qemu 的内核并不完善，所以只能用通过 gpios 转换的方式去实现。正常的内核不需要这样。 源码 gpios 转化中断引脚 参考 drivers\media\rc\gpio-ir-recv.c 中的 rc = request_any_context_irq(gpio_to_irq(pdata->gpio_nr), gpio_ir_recv_irq, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, "gpio-ir-recv-irq", gpio_dev); 修改自己的代码: int gpio; gpio = of_get_gpio(pdev->dev.of_node, 0); g_irq = gpio_to_irq(gpio); error = request_irq(g_irq, input_dev_demo_isr, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, "input_dev_demo_irq", NULL); 调试 cat /proc/interrupts 查看有没有源码中 request_irq(irq->start, input_dev_demo_isr, IRQF_TRIGGER_RISING, "input_dev_demo_irq", NULL); 对应的 "input_dev_demo_irq" 完善触摸驱动，qemu 需要增加 timer, 按下之后，要定时上报数据。 源码 使用定时器来自动上报数据 struct qemu_ts_con { volatile unsigned int pressure; volatile unsigned int x; volatile unsigned int y; volatile unsigned int clean; }; static void ts_irq_timer(unsigned long _data) { if (ts_con->pressure) // pressed { input_event(g_input_dev, EV_ABS, ABS_X, ts_con->x); input_event(g_input_dev, EV_ABS, ABS_Y, ts_con->y); input_sync(g_input_dev); mod_timer(&ts_timer, jiffies + msecs_to_jiffies(TOUCHSCREEN_POLL_TIME_MS)); } } static irqreturn_t input_dev_demo_isr(int irq, void *dev_id) { /* read data */ /* report data */ //input_event(g_input_dev, EV_KEY, XX, 0); //input_sync(g_input_dev); //printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__); if (ts_con->pressure) { input_event(g_input_dev, EV_ABS, ABS_X, ts_con->x); input_event(g_input_dev, EV_ABS, ABS_Y, ts_con->y); input_event(g_input_dev, EV_KEY, BTN_TOUCH, 1); input_sync(g_input_dev); /* start timer */ mod_timer(&ts_timer, jiffies + msecs_to_jiffies(TOUCHSCREEN_POLL_TIME_MS)); } else { input_event(g_input_dev, EV_KEY, BTN_TOUCH, 0); input_sync(g_input_dev); /* cancel timer */ } return IRQ_HANDLED; } 另外，在 probe 和 remove 中需要有 新增和删除定时器。 // probe setup_timer(&ts_timer, ts_irq_timer, (unsigned long)NULL); // remove del_timer_sync(&ts_timer); 调试 ls /dev/input hexdump /dev/input/event3 交叉编译 tslib, 并使用 tslib 中的测试程序来查看效果。 注意，如果想让 tslib 能够扫描到触摸屏，那么驱动中需要增加 __set_bit(INPUT_PROP_DIRECT, g_input_dev->propbit);，有这个标志位， tslib 的扫描函数才能扫描到。 gpio 按键驱动分析 参考资料： Linux 5.x内核 Documentation\devicetree\bindings\input\gpio-keys.txt drivers\input\keyboard\gpio_keys.c Linux 4.x内核 Documentation\devicetree\bindings\input\gpio-keys.txt drivers\input\keyboard\gpio_keys.c 设备树 IMX6ULL：Linux-4.9.88/arch/arm/boot/dts/100ask_imx6ull-14x14.dts STM32MP157：Linux-5.4/arch/arm/boot/dts/stm32mp15xx-100ask.dtsi QEMU：linux-4.9.88/arch/arm/boot/dts/100ask_imx6ull_qemu.dts 框架 右边 input 这边，input_handler 内核里面已经有了，正常不需要自己去动，只需要实现左边，也就是 input_dev 的分配设置注册。 为了方便不同设备的使用，input_dev 的分配设置注册，可以借用万能的 platform_bus 去实现。 platform_bus 也是分为左右两边，左边用设备树实现即可，方便不同设备使用。在系统自动调用的右边的 platform_driver 中再去做 input_dev 的分配设置注册。 设备树 gpio-keys { compatible = "gpio-keys"; pinctrl-names = "default"; user1 { label = "User1 Button"; gpios = <&gpio5 1 GPIO_ACTIVE_LOW>; gpio-key,wakeup; linux,code = <KEY_1>; }; user2 { label = "User2 Button"; gpios = <&gpio4 14 GPIO_ACTIVE_LOW>; gpio-key,wakeup; linux,code = <KEY_2>; }; }; 必备：compatible = "gpio-keys"; 可选： autorepeat: 表示自动重复，按下按键不松开，驱动会自动重复上报按键值 对于每一个GPIO按键，都是一个子节点，有这些属性：其中最重要的就是 gpios 和 linux,code gpios：使用哪个GPIO，GPIO_ACTIVE_LOW 这样的表示低电平有效，空闲时高电平。 interrupts：对应的中断, 只是知道触发边沿了，并不能像使用 gpios 那样，能够确切的知道当前时按下还是松开，没有使用 gpios 好。 linux,code：对应的按键值，用于设置 input_dev 中的 EV_KEY 和 key_bit 注意：gpios和interrupts至少要保留一个，不能都省略 debounce-interval: 消除抖动的间隔，单位：ms，默认是5ms 源码 drivers\input\keyboard\gpio_keys.c 中的 static int gpio_keys_probe(struct platform_device *pdev) 按键配置 pdata = gpio_keys_get_devtree_pdata(dev); 获取设备树按键信息 static struct gpio_keys_platform_data *gpio_keys_get_devtree_pdata(struct device *dev) nbuttons = of_get_available_child_count(node); 获取 key 的个数 pdata = devm_kzalloc(dev, sizeof(*pdata) + nbuttons * sizeof(*button), GFP_KERNEL); 申请空间，按照 key 个数来申请 for_each_available_child_of_node(node, pp) 遍历每个子节点，然后获取 "linux,code", "wakeup-source", "debounce-interval" 等属性。 input = devm_input_allocate_device(dev); platform_set_drvdata(pdev, ddata); input_set_drvdata(input, ddata); input->open = gpio_keys_open; input->close = gpio_keys_close; 配置 input_dev for (i = 0; i < pdata->nbuttons; i++) 循环按键个数 error = gpio_keys_setup_key(pdev, input, bdata, button); 设置 按键 设备树中 gpios 指定按键时，if (gpio_is_valid(button->gpio)) 为真，irq = gpiod_to_irq(bdata->gpiod); 获取 irq, isr = gpio_keys_gpio_isr; 指定 isr 设备树中 interrupts 指定按键时，if (gpio_is_valid(button->gpio)) 为假，bdata->irq = button->irq; 获取 irq, isr = gpio_keys_irq_isr; 指定 isr input_set_capability(input, button->type ?: EV_KEY, button->code); error = devm_request_any_context_irq(&pdev->dev, bdata->irq, isr, irqflags, desc, bdata); 设置中断 error = input_register_device(input); 注册 input_dev 按键中断 gpio_keys_irq_isr if (!bdata->key_pressed) input_event(input, EV_KEY, button->code, 1); 按键按下，会上报按下 if (!bdata->release_delay) input_event(input, EV_KEY, button->code, 0); 如果没有设置 release_delay，那么在上报按下的时候，也会立即上报松开。 if (bdata->release_delay) mod_timer(&bdata->release_timer, jiffies + msecs_to_jiffies(bdata->release_delay)); 如果设置了 release_delay，那么在延迟一段时间后，才上报松开。 这种处理模式，只能处理按键短按，并且上报的松开和实际的按键松开并不会完全一致，所以不是太好。 gpio_keys_gpio_isr mod_delayed_work(system_wq, &bdata->work, msecs_to_jiffies(bdata->software_debounce)); 使用一个延迟的工作线程来做按键的处理工作。使用延迟线程也是为了防止按键抖动，在抖动过程中，只是会推迟处理的时间，等到稳定后延迟到期，才会真正的执行工作线程。 bdata->work 在 gpio_keys_setup_key 中被设置，INIT_DELAYED_WORK(&bdata->work, gpio_keys_gpio_work_func); static void gpio_keys_gpio_work_func(struct work_struct *work) 中调用了 gpio_keys_gpio_report_event(bdata); static void gpio_keys_gpio_report_event(struct gpio_button_data *bdata) 中通过 state = gpiod_get_value_cansleep(bdata->gpiod); 读取按键，然后判断 if (type == EV_ABS) 决定如何上报数据 input_event(input, type, button->code, button->value); input_event(input, type, button->code, state); qemu 设备树 关于按键的设备树，可以把多个按键作为子节点放在一个节点中，这样就只会注册一个 input_dev，也可以都分开，每个按键作为唯一的一个子节点，分布在多个节点中，这样会注册多个 input_dev. 下面这种就是分开来的，会多次调用 probe, 产生多个 input_dev gpio-keys@0 { compatible = "gpio-keys"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_gpio_keys>; status = "okay"; Key0{ label = "Key 0"; gpios = <&gpio5 1 GPIO_ACTIVE_HIGH>; linux,code = <KEY_1>; }; }; gpio-keys@1 { compatible = "gpio-keys"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_gpio_key1>; status = "okay"; Key0{ label = "Key 1"; gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>; linux,code = <KEY_2>; }; }; 编译调试 别忘了，内核中需要开启 Device Drivers->Input device support->Generic input layer->Keyboards->GPIO Buttons 才能让 gpio-keys.c 对应的驱动编译出来。 多个按键调试的时候，可以命令后面加上 & 用作后台运行，减少终端数量。 hexdump /dev/input/event3 & 这样 i2c 接口触摸屏驱动分析 参考资料： Linux 5.x内核 Documentation\devicetree\bindings\input\touchscreen\goodix.txt drivers/input/touchscreen/goodix.c Linux 4.x内核 Documentation\devicetree\bindings\input\touchscreen\goodix.txt drivers/input/touchscreen/gt9xx/gt9xx.c 设备树 IMX6ULL：Linux-4.9.88/arch/arm/boot/dts/100ask_imx6ull-14x14.dts STM32MP157：Linux-5.4/arch/arm/boot/dts/stm32mp15xx-100ask.dtsi 框架 这种框架也是类似的，在 i2c driver 中分配设置注册 input_dev, 同样，这样 i2c driver 也是内核中有，或者厂商提供，我们正常只需要配置设备树，然后系统自动转化为 i2c client. i2c driver 中，除了分配设置注册 input_dev，还要配置中断，然后在中断处理中，使用 i2c_transfer 获取值，然后再上报数值。 设备树 设备树框架 i2c@00000000 { /* ... */ gt928@5d { compatible = "goodix,gt928"; reg = <0x5d>; interrupt-parent = <&gpio>; interrupts = <0 0>; irq-gpios = <&gpio1 0 0>; reset-gpios = <&gpio1 1 0>; }; /* ... */ }; i2c@00000000 指定 i2c 适配器 gt928@5d i2c client, i2c 地址是 5d compatible = "goodix,gt928"; reg = <0x5d>; 用到的 i2c 地址，io 资源 interrupts 相关 实际设备树 &i2c2 { gt9xx@5d { compatible = "goodix,gt9xx"; reg = <0x5d>; status = "okay"; interrupt-parent = <&gpio1>; interrupts = <5 IRQ_TYPE_EDGE_FALLING>; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_tsc_reset &pinctrl_touchscreen_int>; /*pinctrl-1 = <&pinctrl_tsc_irq>;*/ /* pinctrl-names = "default", "int-output-low", "int-output-high", "int-input"; pinctrl-0 = <&ts_int_default>; pinctrl-1 = <&ts_int_output_low>; pinctrl-2 = <&ts_int_output_high>; pinctrl-3 = <&ts_int_input>; */ reset-gpios = <&gpio5 2 GPIO_ACTIVE_LOW>; irq-gpios = <&gpio1 5 IRQ_TYPE_EDGE_FALLING>; irq-flags = <2>; /*1:rising 2: falling*/ touchscreen-max-id = <5>; touchscreen-size-x = <800>; touchscreen-size-y = <480>; touchscreen-max-w = <1024>; touchscreen-max-p = <1024>; /*touchscreen-key-map = <172>, <158>;*/ /*KEY_HOMEPAGE, KEY_BACK*/ goodix,type-a-report = <0>; goodix,driver-send-cfg = <0>; goodix,create-wr-node = <1>; goodix,wakeup-with-reset = <0>; goodix,resume-in-workqueue = <0>; goodix,int-sync = <0>; goodix,swap-x2y = <0>; goodix,esd-protect = <0>; goodix,pen-suppress-finger = <0>; goodix,auto-update = <0>; goodix,auto-update-cfg = <0>; goodix,power-off-sleep = <0>; /* ...... */ }; }; 源码 drivers\input\touchscreen\gt9xx\gt9xx.c gtp_probe static int gtp_probe(struct i2c_client *client, const struct i2c_device_id *id) ret = gtp_request_input_dev(ts); 分配设置注册 input_dev ret = gtp_request_irq(ts); 设置中断 static int gtp_request_irq(struct goodix_ts_data *ts) ret = request_threaded_irq(ts->client->irq, NULL, gtp_irq_handler, ts->pdata->irq_flags | IRQF_ONESHOT, ts->client->name, ts); 注册线程化中断处理函数，中断下半部使用线程来处理。中断上半截是 NULL，中断下半截是 gtp_irq_handler 中断 static irqreturn_t gtp_irq_handler(int irq, void *dev_id) 中调用 gtp_work_func(ts); static void gtp_work_func(struct goodix_ts_data *ts) 中是实际的中断处理 point_state = gtp_get_points(ts, points, &key_value); 获取 i2c 数据。 gtp_get_points -> gtp_i2c_read -> i2c_transfer gtp_mt_slot_report(ts, point_state & 0x0f, points); 上报触摸数据。 input_mt_slot(ts->input_dev, points->id); input_mt_report_slot_state(ts->input_dev, MT_TOOL_PEN, true); input_report_abs(ts->input_dev, ABS_MT_POSITION_X, points->x); UInput分析 UInput 属于用户可以用一个模拟的输入设备进行输入。 参考资料： Linux 5.x内核 Documentation\input\uinput.rst drivers\input\misc\uinput.c Linux 4.x内核 内核没有对应文档 drivers\input\misc\uinput.c 框架 左侧 app 通过 uinput 的驱动程序，open, ioctl 分配设置注册了 input_dev; 通过 write 写入数据，然后驱动程序通过 input_event 上报数据。右侧的 app 就可以直接使用这些按键数据。 源码 drivers\input\misc\uinput.c uinput_open static int uinput_open(struct inode *inode, struct file *file) uinput_ioctl static long uinput_ioctl(struct file *file, unsigned int cmd, unsigned long arg) -> uinput_ioctl_handler(file, cmd, arg, (void __user *)arg); static long uinput_ioctl_handler(struct file *file, unsigned int cmd, unsigned long arg, void __user *p) retval = uinput_allocate_device(udev); 其中 udev->dev = input_allocate_device(); input_set_drvdata(udev->dev, udev); 分配 input_dev switch (cmd) 这里面就是根据 cmd 来设置 input 相关位。 app 里面正常可以使用以下这些设置: ioctl(fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT); 触摸屏模式 ioctl(fd, UI_SET_EVBIT, EV_KEY); ioctl(fd, UI_SET_KEYBIT, BTN_TOUCH); 使能按键 ioctl(fd, UI_SET_EVBIT, EV_ABS); ioctl(fd, UI_SET_ABSBIT, ABS_X); ioctl(fd, UI_SET_ABSBIT, ABS_Y); 使能触摸。 ioctl(fd, UI_DEV_SETUP, &usetup); ioctl(fd, UI_DEV_CREATE); 设置并创建 uinput. app 中的 UI_DEV_CREATE 对应的是 uinput_create_device(udev);, static int uinput_create_device(struct uinput_device *udev) error = input_register_device(udev->dev); 注册 input_dev app 中的 ioctl(fd, UI_DEV_DESTROY); 对应的是 uinput_destroy_device(udev); app 中的 close(fd); 对应的是 uinput_release 函数，其中也调用了 uinput_destroy_device(udev); uinput_write static ssize_t uinput_write(struct file *file, const char __user *buffer, size_t count, loff_t *ppos) uinput_inject_events(udev, buffer, count) input_event_from_user(buffer + bytes, &ev) 获取数据 input_event(udev->dev, ev.type, ev.code, ev.value); 上报数据。 调试 别忘了内核要开 Device Drivers -> Input device support -> Generic input layer -> Miscellaneous devices -> <M> User level driver support 如果设备上已经有了一个真实的触摸屏，那么在开启 uinput 之后，需要通过 export TSLIB_TSDEVICE=/dev/input/event3 来手动指定用哪个触摸屏用于 app 测试。 100ASK_IMX6ULL板子上支持其他型号的屏幕 lcd 找到 imx 厂家对应 lcd 尺寸的 dts 移植 lcd 节点相关内容到板子对应的 dts 如果有需要，可以按照 lcd 和 芯片规格书，调整 lcd 时序等参数。 调试 如果想要调试复位引脚: # GPIO3_IO04在GPIO子系统中编号为：(3-1)*32+4=68，它是第68号GPIO。 $ fb-test // LCD上应该显示红绿蓝色块 $ echo 68 > /sys/class/gpio/export // 导出68号GPIO $ echo out > /sys/class/gpio/gpio68/direction // 设置为输出引脚 $ echo 0 > /sys/class/gpio/gpio68/value // 让它输出0 $ echo 1 > /sys/class/gpio/gpio68/value // 让它输出1 $ echo 68 > /sys/class/gpio/unexport // unexport 如果手动调试复位引脚，能够改善屏幕，那就需要在 dts 中，增加复位引脚。 dts reset-gpios = <&gpio3 4 GPIO_ACTIVE_LOW>; /* 100ask */ 源码 /* 100ask */ printk("100ask, %s %s %d\n", __FILE__, __FUNCTION__, __LINE__); rst_gpio = of_get_named_gpio(pdev->dev.of_node, "reset-gpios", 0); if (gpio_is_valid(rst_gpio)) { ret = gpio_request(rst_gpio, "lcdif_rst"); if (ret < 0) { dev_err(&pdev->dev, "Failed to request GPIO:%d, ERRNO:%d\n", (s32)rst_gpio, ret); } else { gpio_direction_output(rst_gpio, 0); msleep(2); gpio_direction_output(rst_gpio, 1); dev_info(&pdev->dev, "Success seset LCDIF\n"); } } 触摸屏 有厂商信号 直接按照厂商规格书来。 无厂商信号 i2cdetect -y 1 查看 i2c 总线上面的设备，通过插拔触摸屏，确定 i2c 地址 arch/arm/boot/dts/ 下面， grep 一下， i2c 地址。可以参考这个芯片对应的 dts 节点。 i2c 控制器 先找找 soc 厂商的 dts 中有没有这个 i2c 地址相关的节点，有的话，就复制过来，然后按照现有触摸屏节点，或者 pcba 去修改相关的引脚。 源码 根据 dts 中，哪个 i2c 的 compatible 去 drivers/input/touchscreen/ 中搜索有没有匹配的驱动源码。 找到源码之后，去内核中，开启这个驱动选项 tslib 调试 先关闭现有的屏幕程序 mv /etc/init.d/S07hmi /root/, 然后 reboot 执行 ts_test_mt, 简单看下触摸情况 export TSLIB_TSDEVICE=/dev/input/event1 export TSLIB_CONFFILE=/etc/ts.conf export TSLIB_CALIBFILE=/etc/pointercal export TSLIB_PLUGINDIR=/usr/lib/ts ts_test_mt ts_print_raw 有更加详细的触摸数据打印。 xy 对调 配置文件 /etc/ts.conf 中新增 module linear xyswap 设备树 dts 中，新增 touchscreen-swapped-x-y=<1>; xy 反向 配置文件 /etc/ts.conf 中新增 module invert x0=1024 y0=600, x 0和1024对调，y 0和600对调 设备树 可以参考 Documentation/devicetree/bindings/input/touchscreen dts 中，新增 touchscreen-inverted-x = <1>; touchscreen-inverted-y = <1>; 固件更新 如果要禁止驱动去修改配置信息： goodix,driver-send-cfg = <0>; 总结 正常情况下，input 只需要修改 dts。 特殊情况下，需要新增一个驱动，在驱动中，读取 dts，然后分配设置注册 input_dev，中断里面上报数据即可。 如果是 i2c 的，那么用 i2c_client 那一套；如果是 gpio 那一套，那就走 platform_bus 那一套。
当 iar 工程在多个地方复制使用的时候，有时候定位函数的时候，会提示错误的路径。 解决方法： project --> clean，清除， 然后 rebuild all，就可以了。
win10 上面的微软拼音有多种 双拼方案，也可以自定义方案，因为习惯紫光方案，所以自定义修改即可。 [...]
I2C 介绍 i2c device driver: 如何读写数据，组织命令数据等，比如命令中需要包含命令、地址等数据，还有收到原始数据后如何解析。 i2c controller driver: 如何按照 i2c 的协议，来传输 device driver 的数据。 i2c 数据通道 普通用法, 芯片有 i2c 控制器，使用 i2c 设备驱动: app <-> some driver <-> adapt driver <-> device 芯片没有 i2c 控制器，使用 gpio 模拟i2c 控制，使用 i2c 设备驱动: app <-> some driver <-> i2c-gpio.c <-> device app 希望自己组织数据，跳过 i2c 设备驱动，来使用 i2c 控制器驱动: app <-> i2c-dev.c <-> adapt driver <-> device app 希望自己组织数据，跳过 i2c 设备驱动，使用 gpio 模拟i2c驱动: app <-> i2c-dev.c <-> i2c-gpio.c <-> device app 使用 i2c-dev.c 自己组织数据，这种一般成为用户态驱动程序。 SMBus 协议 SMBus 是 i2c 的子集。在以下方面要求更加严格: 电压范围，1.8 - 5V 最小时钟频率 10KHz，最大的 SCL 拉低占用时间也有限制 开始的地址匹配时，必须 ack，用于判断设备状态是否正常。 增加 restart 信号，可以省略停止信号。 SMBus 和 i2c 的最大区别，是 block r/w 的时候，SMBus 要求在寄存器地址后，正式数据前，必须增加一个长度数据。 i2c 系统的重要结构体 i2c_adapter controller 对应的结构体是 i2c_adapter, 主要的成员是：第几个i2c控制器， 传输函数。 struct i2c_adapter { // 传输函数 const struct i2c_algorithm *algo; /* the algorithm to access the bus */ // 第几个 bus 上的控制器 int nr; i2c_client device 对应的结构体是 i2c_client，最重要的成员是: 设备地址。 struct i2c_client { // 可以用来表示少部分的 10位地址，或者其他情况。 unsigned short flags; /* div., see below */ unsigned short addr; /* chip address - NOTE: 7bit */ /* addresses are stored in the */ /* _LOWER_ 7 bits */ // device 具体挂载在哪个总线对应的 adapter 下面? struct i2c_adapter *adapter; /* the adapter we sit on */ i2c_msg int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs, int num); 传输函数中，第一个参数 adap 指明了是第几个 i2c 控制器；第二个参数是传输数据，其中包含了芯片地址、数据 buf、数据长度；第三个参数 num 是 msg 的个数，比如读取数据，那么就需要两个 msg，前一个是寄存器地址 msg，第二个是数据读取命令 msg，所以需要一次创建好多个 msg. struct i2c_msg { __u16 addr; /* slave address */ // flags 标识 read / write __u16 flags; __u16 len; /* msg length */ __u8 *buf; /* pointer to msg data */ }; i2c_transfer 直接使用 master_xfer 传输比较麻烦，可以使用 int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num) 来传输，内部调用了 master_xfer. 无需驱动直接访问 i2c 正常情况，需要自己写 i2c 驱动程序，然后 app 通过驱动程序来工作。 但是，如果不像自己写 i2c 驱动程序，那么可以通过内核自带驱动 i2c-dev.c 再通过 i2c_adapter 就可以和 i2c 设备进行通信。 app 如何使用 i2c-dev.c, 可以参考 i2c-tools 这个软件。 i2c-tools 操作 i2c，需要确定三个东西: 哪个 adapter, 哪个 address, 具体操作数据？ i2cdetect i2cdetect 命令用于检测总线上是否有 i2c 设备。 i2cdetect 0 用于检测 0号总线上的设备。 i2cdetect -y 0 提前输入 y i2cdetect 返回的位置上，UU 表明有设备并且有驱动，1e 这样的非 UU 表明有设备没驱动。 i2cdetect -l 列出当前总线列表。 smbus 传输数据 对于 UU 这样已经有驱动的设备，不能直接操作，必须加上 -f 才能进行操作。 i2cset -f -y 0 0x1e 0 0x4 : 向 0号总线的 0x1e 设备的 0 号地址写入 0x4 i2cget -f -y 0 0x1e 0xc w : 从 0号总线的 0x1e 设备的 0xc 号地址读取一个 word i2c 传输数据 i2ctransfer -f -y 0 w2@0x1e 0 0x4 : 向 0号总线的 0x1e 设备的 0 号地址写入2个字节 0x4 i2ctransfer -f -y 0 w1@0x1e 0xc r2 : 从 0号总线的 0x1e 设备的 0xc 号地址读取2个字节。 访问设备 open ls /dev/i2c-* 得到的是 i2c_adapter 控制器，是和 i2cdetect -l 一样的，只是比较简略。 ioctl ioctl(file, I2C_SLAVE, address) ioctl(file, I2C_SLAVE_FORCE, address) 指定具体是哪个 i2c 设备。force 针对的是有驱动的 i2c 设备。 ioctl(file, I2C_SMBUS, &args) 使用 smbus 传输数据。 ioctl(file, I2C_RDWR, &rdwr) 使用 i2c 传输数据。 源码 include\uapi\linux\i2c-dev.h 中的 struct i2c_rdwr_ioctl_data 用于封装使用 ioctl 通过 i2c 来传输的数据。 使用 i2c 的源码流程 使用 smbus 的源码流程 i2c-tools 交叉编译 i2c-tools-4.2.tar.xz 源码 修改 Makefile CC = $(CROSS_COMPILE)gcc AR = $(CROSS_COMPILE)ar STRIP = $(CROSS_COMPILE)strip export 相关环境变量 export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf- export PATH=$PATH:/home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/bin 执行make时，是动态链接，需要把libi2c.so也放到单板上；静态链接的话，执行：`make USE_STATIC_LIB=1 编写 app 访问 eeprom app 可以使用 i2c-tools 里面提供的 api: int open_i2c_dev(int i2cbus, char *filename, size_t size, int quiet); int set_slave_addr(int file, int address, int force); extern __s32 i2c_smbus_write_byte(int file, __u8 value); extern __s32 i2c_smbus_read_i2c_block_data(int file, __u8 command, __u8 length, __u8 *values); ... 编译的时候，需要增加下面这些文件: smbus.c smbus.h, i2cbusses.c i2cbusses.h . app 中需要注意，当连续读写的时候，需要增加 nanosleep 增加延时，满足 i2c 设备的时序要求。 通用驱动 i2c-dev 分析 i2c_dev_init register_chrdev_region 注册一个区域，res = register_chrdev_region(MKDEV(I2C_MAJOR, 0), I2C_MINORS, "i2c"); res = bus_register_notifier(&i2c_bus_type, &i2cdev_notifier); 注册 i2c bus 通知的回调函数结构体。在 static struct notifier_block i2cdev_notifier 中，定义了 .notifier_call = i2cdev_notifier_call, 在 static int i2cdev_notifier_call(struct notifier_block *nb, unsigned long action, void *data) 中，会识别 action 是增加还是减少设备，从而调用 i2cdev_attach_adapter 或者 i2cdev_detach_adapter i2c_for_each_dev(NULL, i2cdev_attach_adapter); 绑定所有已经存在的 i2c 总线控制设备。 i2cdev_attach_adapter cdev_init(&i2c_dev->cdev, &i2cdev_fops); res = cdev_add(&i2c_dev->cdev, MKDEV(I2C_MAJOR, adap->nr), 1); 主设备号: I2C_MAJOR, 次设备号: adap->nr, 也就是 /dev/i2c-n 中的 n. 字符设备注册到内核中，使得内核可以识别和管理该设备， 提供设备的内核层面功能（如 file_operations 的实现）。 关联字符设备与设备号（dev_t）, 使得设备可以通过 /dev 目录下的设备文件进行访问。 i2c_dev->dev = device_create(i2c_dev_class, &adap->dev, MKDEV(I2C_MAJOR, adap->nr), NULL, "i2c-%d", adap->nr); 在用户空间创建设备文件（如 /dev/my_device），使得应用程序可以访问设备。 在 /sys 目录下创建设备的属性文件，提供设备的管理接口。 负责设备的用户空间接口创建。使得设备可以通过用户空间工具（如 udev）进行管理。 i2cdev_open static int i2cdev_open(struct inode *inode, struct file *file) adap = i2c_get_adapter(minor); 从 inode 中拿到次设备号，然后获取相应的 adapter. client = kzalloc(sizeof(*client), GFP_KERNEL); client->adapter = adap; adap 放到 client 里面。 file->private_data = client; 把 client 放到 file 私有数据里。 i2cdev_ioctl static long i2cdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg) cmd I2C_SLAVE, struct i2c_client *client = file->private_data; client->addr = arg; 设置i2c设备地址。 cmd I2C_RDWR, static noinline int i2cdev_ioctl_rdwr(struct i2c_client *client, unsigned long arg) i2c 模式读写数据。 res = i2c_transfer(client->adapter, rdwr_pa, rdwr_arg.nmsgs); i2c 模式传输数据。 cmd I2C_SMBUS, static noinline int i2cdev_ioctl_smbus(struct i2c_client *client, unsigned long arg) smbus 模式读写数据 res = i2c_smbus_xfer(client->adapter, client->addr, client->flags, data_arg.read_write, data_arg.command, data_arg.size, &temp); smbus 模式传输数据。 I2C系统驱动程序模型 参考资料 内核文档: Documentation\i2c\instantiating-devices.rst Documentation\i2c\writing-clients.rst 内核驱动程序示例: drivers/eeprom/at24.c 层次图 总线设备驱动模型 源码 i2c_driver static struct i2c_driver at24_driver 中 .driver.of_match_table 和 .id_table 用于匹配。 匹配函数来源于 i2c-core.c 中的 struct bus_type i2c_bus_type 中的 .match = i2c_device_match, 匹配的顺序是 i2c_driver 中的 .driver.of_match_table > .driver.acpi_match_table > .id_table static int i2c_device_match(struct device *dev, struct device_driver *drv) { ... /* Attempt an OF style match */ if (of_driver_match_device(dev, drv)) return 1; /* Then ACPI style match */ if (acpi_driver_match_device(dev, drv)) return 1; driver = to_i2c_driver(drv); /* match on an id table if there is one */ if (driver->id_table) return i2c_match_id(driver->id_table, client) != NULL; ... } static inline int of_driver_match_device(struct device *dev, const struct device_driver *drv) 调用的函数，不同的 linux 内核版本也不一样，有些是调用 i2c_of_match_device 里面包含 of_match_device 和 i2c_of_match_device ; 有些是 of_match_device -> of_match_node -> __of_match_node -> __of_device_is_compatible 然后对比顺序为 compatible > type > name acpi_driver_match_device 正常不用关心 static const struct i2c_device_id *i2c_match_id(const struct i2c_device_id *id, const struct i2c_client *client) 直接使用 name 进行比较。 创建一个 client 通过设备树创建，或者通过 i2c_register_board_info 创建. 但是i2c_register_board_info不会导出内核符号表，不能作为模块编译，只能编译进内核。 通过 i2c_new_device 或者 i2c_new_probed_device 来创建，建议使用 i2c_new_probed_device 会检查设备是否存在。 用户空间使用命令来创建，方便调试 // 创建一个i2c_client, .name = "eeprom", .addr=0x50, .adapter是i2c-3 # echo eeprom 0x50 > /sys/bus/i2c/devices/i2c-3/new_device // 删除一个i2c_client # echo 0x50 > /sys/bus/i2c/devices/i2c-3/delete_device 编写设备驱动 i2c_driver i2c_driver 的框架，类似于普通的字符设备的框架，compatible 匹配，probe 里面，先是 register_chrdev, 然后是 class_create 和 device_create. 在 remove 当中，就是反过来的操作。 编写设备驱动 i2c_client 编译为模块 增加 Makefile. KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88/ all: make -C $(KERN_DIR) M=`pwd` modules clean: make -C $(KERN_DIR) M=`pwd` modules clean rm -rf modules.order obj-m += ap3216c_drv.o obj-m += ap3216c_client.o 命令生成 client // 在I2C BUS0下创建i2c_client # echo ap3216c 0x1e > /sys/bus/i2c/devices/i2c-0/new_device // 删除i2c_client # echo 0x1e > /sys/bus/i2c/devices/i2c-0/delete_device 源码 client 使用 i2c_new_device 或者 i2c_new_probed_device 来创建 设备树 client 设备树里i2c1就是I2C BUS0。 &i2c1 { ap3216c@1e { compatible = "lite-on,ap3216c"; reg = <0x1e>; }; }; make dtbs cp /mnt/100ask_imx6ull-14x14.dtb /boot sync I2C_Adapter驱动框架 i2c_adapter struct i2c_adapter 中，algo 是传输方法，nr 是第几条总线。 struct i2c_adapter { const struct i2c_algorithm *algo; /* the algorithm to access the bus */ ... int nr; ... }; i2c_algorithm struct i2c_algorithm 中，最重要的是 master_xfer，这个是 i2c 的最基础的传输函数; smbus_xfer 用于 smbus 传输，如果不提供这个函数，会使用 master_xfer 来模拟传输 smbus; functionality 用于获取支持的功能，返回值通过位标识功能，一般在 i2c.h 以宏 I2C_FUNC_ 开头。 struct i2c_algorithm { int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs, int num); int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr, unsigned short flags, char read_write, u8 command, int size, union i2c_smbus_data *data); ... u32 (*functionality) (struct i2c_adapter *); }; /* To determine what functionality is present */ #define I2C_FUNC_I2C 0x00000001 #define I2C_FUNC_10BIT_ADDR 0x00000002 #define I2C_FUNC_PROTOCOL_MANGLING 0x00000004 /* I2C_M_IGNORE_NAK etc. */ #define I2C_FUNC_SMBUS_PEC 0x00000008 #define I2C_FUNC_NOSTART 0x00000010 /* I2C_M_NOSTART */ #define I2C_FUNC_SLAVE 0x00000020 #define I2C_FUNC_SMBUS_BLOCK_PROC_CALL 0x00008000 /* SMBus 2.0 */ #define I2C_FUNC_SMBUS_QUICK 0x00010000 #define I2C_FUNC_SMBUS_READ_BYTE 0x00020000 #define I2C_FUNC_SMBUS_WRITE_BYTE 0x00040000 #define I2C_FUNC_SMBUS_READ_BYTE_DATA 0x00080000 #define I2C_FUNC_SMBUS_WRITE_BYTE_DATA 0x00100000 #define I2C_FUNC_SMBUS_READ_WORD_DATA 0x00200000 #define I2C_FUNC_SMBUS_WRITE_WORD_DATA 0x00400000 #define I2C_FUNC_SMBUS_PROC_CALL 0x00800000 #define I2C_FUNC_SMBUS_READ_BLOCK_DATA 0x01000000 #define I2C_FUNC_SMBUS_WRITE_BLOCK_DATA 0x02000000 #define I2C_FUNC_SMBUS_READ_I2C_BLOCK 0x04000000 /* I2C-like block xfer */ #define I2C_FUNC_SMBUS_WRITE_I2C_BLOCK 0x08000000 /* w/ 1-byte reg. addr. */ #define I2C_FUNC_SMBUS_HOST_NOTIFY 0x10000000 源码 可以参考: i2c-gpio.c 这种模拟i2c控制的源码去写。也可以参考 i2c-imx.c. static int i2c_bus_virtual_probe(struct platform_device *pdev) { /* get info from device tree, to set i2c_adapter/hardware */ /* alloc, set, register i2c_adapter */ g_adapter = kzalloc(sizeof(*g_adapter), GFP_KERNEL); g_adapter->owner = THIS_MODULE; g_adapter->class = I2C_CLASS_HWMON | I2C_CLASS_SPD; g_adapter->nr = -1; snprintf(g_adapter->name, sizeof(g_adapter->name), "i2c-bus-virtual"); g_adapter->algo = &i2c_bus_virtual_algo; i2c_add_adapter(g_adapter); // i2c_add_numbered_adapter(g_adapter); return 0; } 完善虚拟的I2C_Adapter 使用GPIO模拟I2C 设备树 不同版本的内核，设备树的写法也有点差别。 源码 i2c-gpio.c 中的 probe，在设备树相关的读取之后，调用了 i2c-algo-bit.c 中的 i2c_bit_add_numbered_bus 来做传输函数的添加。 i2c-algo-bit.c static int __i2c_bit_add_bus(struct i2c_adapter *adap, int (*add_adapter)(struct i2c_adapter *)) 把传输函数给关联上了。 adap->algo = &i2c_bit_algo; const struct i2c_algorithm i2c_bit_algo = { .master_xfer = bit_xfer, .functionality = bit_func, }; static int bit_xfer(struct i2c_adapter *i2c_adap, struct i2c_msg msgs[], int num) 中的 i2c_start(adap); 也非常简单。 static void i2c_start(struct i2c_algo_bit_data *adap) { /* assert: scl, sda are high */ setsda(adap, 0); udelay(adap->udelay); scllo(adap); } 使用 i2c-gpio 如果想用 i2c-gpio 来控制一个新的设备，只需要在设备树中，增加一个新的 i2c 节点。 compatible = "i2c-gpio"; 使用pinctrl把 SDA、SCL所涉及引脚配置为GPIO、开极 可选 指定SDA、SCL所用的GPIO 指定频率(2种方法)： i2c-gpio,delay-us = <5>; /* ~100 kHz */ clock-frequency = <400000>; #address-cells = <1>; #size-cells = <0>; i2c-gpio,sda-open-drain： 它表示其他驱动、其他系统已经把SDA设置为open drain了 在驱动里不需要在设置为open drain 如果需要驱动代码自己去设置SDA为open drain，就不要提供这个属性 i2c-gpio,scl-open-drain： 它表示其他驱动、其他系统已经把SCL设置为open drain了 在驱动里不需要在设置为open drain 如果需要驱动代码自己去设置SCL为open drain，就不要提供这个属性 使用GPIO操作I2C设备 使用 gpio，需要确定内核里面的 i2c gpio 已经开启，或者作为模块开启才行。 具体芯片的I2C_Adapter imx6ull 对应的是 i2c-imx.c i2c_imx_start imx_i2c_write_reg(i2c_imx->ifdr, i2c_imx, IMX_I2C_IFDR); 这样的写入寄存器的语句，可以看到寄存器的地址来源于 i2c_imx->base 和 reg << i2c_imx->hwdata->regshift, 不同芯片，每个寄存器之间的地址间隔不一样，所以 i2c_imx->hwdata->regshift 是按照具体芯片地址间隔设定的结构体，所以，在写入寄存器的时候，只需要提供偏移多少位的 reg 即可。 static inline void imx_i2c_write_reg(unsigned int val, struct imx_i2c_struct *i2c_imx, unsigned int reg) { writeb(val, i2c_imx->base + (reg << i2c_imx->hwdata->regshift)); } static const struct imx_i2c_hwdata imx1_i2c_hwdata = { .devtype = IMX1_I2C, .regshift = IMX_I2C_REGSHIFT, .clk_div = imx_i2c_clk_div, .ndivs = ARRAY_SIZE(imx_i2c_clk_div), .i2sr_clr_opcode = I2SR_CLR_OPCODE_W0C, .i2cr_ien_opcode = I2CR_IEN_OPCODE_1, }; i2c_imx_xfer if (msgs[i].flags & I2C_M_RD) 判断这个 flag，然后决定 i2c_imx_read 或者 i2c_imx_write，数据收发完成之后，就会调用 i2c_imx_stop(i2c_imx) i2c_imx_read static int i2c_imx_read(struct imx_i2c_struct *i2c_imx, struct i2c_msg *msgs, bool is_lastmsg) 使用 imx_i2c_write_reg((msgs->addr << 1) | 0x01, i2c_imx, IMX_I2C_I2DR); 之后，就会用 result = i2c_imx_trx_complete(i2c_imx); 来等待完成 static int i2c_imx_trx_complete(struct imx_i2c_struct *i2c_imx) 中，调用 wait_event_timeout(i2c_imx->queue, i2c_imx->i2csr & I2SR_IIF, HZ / 10); 来等待。 for 循环读取数据的时候，也会调用 i2c_imx_trx_complete 来等待。 总结 i2c 其实主要是两层，上层是 i2c_adapter, 下层是 i2c_client. i2c_adapter 对应的是芯片的 i2c 控制器， i2c_client 对应的是实际的 i2c 设备。 i2c_adapter 有相应的驱动，比如 i2c-imx.c，i2c_client 也有相应的驱动，比如 at24.c 设备树中，i2c 控制器本身的节点有 compatible 用于和 i2c_adapter 适配，i2c 的子节点也有 compatible 用于和 i2c_client 适配。 i2c_adapter 驱动中实现了 master_xfer, functionality 函数。用于比如说 ARM，使用自身 i2c 控制器的方法。 在适配设备树的时候，根据设备树的父子节点，来自动把 client 挂载在 adapter 上面。比如, i2c2 这个会根据 compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c"; 匹配到 adapter 上面; wm8960 会根据 "wlf,wm8960" 匹配到 client 上面，并且会自动把 adapter 放到 client->adapter, 这个自动匹配是在 i2c_new_device() 里面的，由系统自动调用。 i2c2: i2c@021a4000 { #address-cells = <1>; #size-cells = <0>; compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c"; reg = <0x021a4000 0x4000>; interrupts = <GIC_SPI 37 IRQ_TYPE_LEVEL_HIGH>; clocks = <&clks IMX6UL_CLK_I2C2>; status = "disabled"; }; &i2c2 { clock_frequency = <100000>; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_i2c2>; status = "okay"; codec: wm8960@1a { compatible = "wlf,wm8960"; reg = <0x1a>; clocks = <&clks IMX6UL_CLK_SAI2>; clock-names = "mclk"; wlf,shared-lrclk; }; 如果在 at24.c 这里面实现了 sysfs 获得 devfs，那么就可以直接使用文件接口来操作；如果没有实现这些接口，那么就需要 app 自己处理协议组织数据，然后通过 ioctl 来操作，间接调用了 i2c_transfer。 如果 app 直接通过 i2c-dev.c 来操作，一般需要通过 open_i2c_dev set_slave_addr i2c_smbus_write_byte_data 直接操作，当然中间需要通过 adapter 的驱动。可以参考: at24c02_test. 如果 app 如果专门的设备驱动来操作，那么这种设备驱动其实就是在 open, read, write 中，调用 open_i2c_dev set_slave_addr i2c_smbus_write_byte_data 这些函数。设备驱动向上提供了 sysfs 或者 devfs 接口。这样 app 就可以通过 open, read, write 这样的 fs 接口来操作。可以参考: ap3216c_ok 总之，首先要确认的就是有没有 soc 的总线控制器也就是 adapter 的驱动，然后才能决定下一步。 有 adapter 驱动 不需要编写任何驱动，只需通过用户空间应用程序（如 i2c-tools 或自定义程序）直接操作 /dev/i2c-* 设备文件即可。 启用 I2C 控制器驱动 确保内核已包含对应 I2C 控制器的驱动（例如树莓派的 i2c-bcm2835） 在设备树（Device Tree）中启用 I2C 控制器节点（如设置 status = "okay"）。 加载 i2c-dev 模块 modprobe i2c-dev # 生成 /dev/i2c-0, /dev/i2c-1 等设备文件 用户空间应用程序直接操作：使用 ioctl 调用（如 I2C_RDWR）发送 I2C 消息。 #include <linux/i2c-dev.h> #include <fcntl.h> #include <sys/ioctl.h> #include <unistd.h> #include <stdio.h> int main() { int fd = open("/dev/i2c-1", O_RDWR); // 打开 I2C 总线设备文件 if (fd < 0) { perror("open"); return -1; } // 设置从设备地址（例如 0x50） if (ioctl(fd, I2C_SLAVE, 0x50) < 0) { perror("ioctl"); close(fd); return -1; } // 写入数据到设备（例如写入寄存器地址 0x00） unsigned char reg_addr = 0x00; if (write(fd, &reg_addr, 1) != 1) { perror("write"); close(fd); return -1; } // 从设备读取数据（例如读取 2 字节） unsigned char buf[2]; if (read(fd, buf, 2) != 2) { perror("read"); close(fd); return -1; } printf("Data: 0x%02x 0x%02x\n", buf[0], buf[1]); close(fd); return 0; } adapter 驱动不存在 如果目标 I2C 控制器（如某款自定义硬件）的驱动未在内核中实现，则需要 编写 I2C 适配器驱动，但 不需要编写具体设备的驱动。 I2C 适配器驱动（i2c_adapter） 实现 i2c_algorithm 结构体中的 master_xfer 方法，定义如何通过硬件发送 I2C 消息。 注册适配器（i2c_add_adapter），使其出现在 /sys/bus/i2c/devices/ 并生成 /dev/i2c-* 设备文件。 设备树或平台设备配置： 在设备树中定义 I2C 控制器的寄存器地址、中断号等硬件信息。 或通过平台设备（platform_device）静态注册。 #include <linux/i2c.h> #include <linux/platform_device.h> static struct i2c_algorithm my_i2c_algo = { .master_xfer = my_i2c_xfer, // 实现 I2C 传输函数 }; static int my_i2c_probe(struct platform_device *pdev) { struct i2c_adapter *adap; adap = devm_kzalloc(&pdev->dev, sizeof(*adap), GFP_KERNEL); adap->owner = THIS_MODULE; adap->algo = &my_i2c_algo; adap->dev.parent = &pdev->dev; snprintf(adap->name, sizeof(adap->name), "my-i2c-adapter"); i2c_add_adapter(adap); // 注册适配器 return 0; } static struct platform_driver my_i2c_driver = { .driver = { .name = "my-i2c-controller", }, .probe = my_i2c_probe, }; module_platform_driver(my_i2c_driver); 是否需要编写具体设备驱动？ 不需要：i2c-dev 的用途正是为了绕过内核驱动，允许用户空间直接操作 I2C 设备。设备驱动（如传感器驱动）的作用是将硬件功能封装为内核接口（如 sysfs、字符设备），但若你选择直接通过 /dev/i2c-* 操作，则无需在内核中实现这些逻辑。 总结 场景 需实现内容 用户空间操作方式 I2C 控制器驱动已存在 无需编写任何驱动 直接通过 /dev/i2c-* 操作 I2C 控制器驱动不存在 需编写适配器驱动 适配器驱动注册后，通过 /dev/i2c-* 操作