参考: STM32F103 通过SD卡IAP升级程序，带MD5校验，（带源码）可在实际项目中使用 https://blog.csdn.net/qq_15181569/article/details/106852095 文件md5在线计算 http://www.metools.info/other/o21.html STM32F103通过Ymodem协议更新程序带MD5校验 https://blog.csdn.net/qq_15181569/article/details/115762289
总结 数据采集流程 设置好长宽之后，摄像头可能不支持，需要检查长宽参数是否被修改。 app 根据自身处理能力的快慢，指定申请的 buffer 数量。 一般使用链表的形式来组织 buffer，app 从 out 链表中拿数据，拿完之后，放入 in 链表；驱动从 in 链表中拿到 buffer，然后填充数据，再放到 out 链表中去。 app 开发 关键结构体: v4l2_fmtdesc format enumeration v4l2_format stream data format ioctl 中常用的 id 是: VIDIOC_QUERYCTRL 查找参数 VIDIOC_G_CTRL 获取当前参数 VIDIOC_S_CTRL 设置当前参数 对于 VideoControl interface 内部，有多个 unit。 unit 自身只能多个输入，单个输出。 unit之间，只能单个 unit 输出给多个 unit. VideoControl interface 内部有 CT(camera terminal), IT(input terminal), OT(out terminal), SU(selector unit), PU(processing unit), EU(encoding unit), XU(extension unit) VideoStreaming interface 是 USB 相关 如果自己做摄像头，想要增加额外功能，那就需要用到 XU，并且还需要添加 mapping，指定具体哪些位是哪些功能。 app 帧细节 格式枚举(yuv 还是其他)，用 VIDIOC_ENUM_FMT; 帧枚举(分辨率之类)用 VIDIOC_ENUM_FRAMESIZES, 都需要让相应的 index 增加来通过 ioctl 枚举。 app 获取数据 检查设备能力 struct v4l2_capability cap; memset(&cap, 0, sizeof(struct v4l2_capability)); if (0 == ioctl(fd, VIDIOC_QUERYCAP, &cap)) { if((cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) == 0) { 枚举格式和帧VIDIOC_ENUM_FMT VIDIOC_ENUM_FRAMESIZES 设置格式 struct v4l2_format fmt; memset(&fmt, 0, sizeof(struct v4l2_format)); fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width = 1024; fmt.fmt.pix.height = 768; fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; fmt.fmt.pix.field = V4L2_FIELD_ANY; if (0 == ioctl(fd, VIDIOC_S_FMT, &fmt)) 申请buffer 并 mmap struct v4l2_requestbuffers rb; memset(&rb, 0, sizeof(struct v4l2_requestbuffers)); rb.count = 32; rb.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; rb.memory = V4L2_MEMORY_MMAP; if (0 == ioctl(fd, VIDIOC_REQBUFS, &rb)) { /* 申请成功后, mmap这些buffer */ buf_cnt = rb.count; for(i = 0; i < rb.count; i++) { struct v4l2_buffer buf; memset(&buf, 0, sizeof(struct v4l2_buffer)); buf.index = i; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; if (0 == ioctl(fd, VIDIOC_QUERYBUF, &buf)) { /* mmap */ bufs[i] = mmap(0 /* start anywhere */ , buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); if(bufs[i] == MAP_FAILED) { perror("Unable to map buffer"); return -1; } } 所有buffer放入"空闲链表" for(i = 0; i < buf_cnt; ++i) { struct v4l2_buffer buf; memset(&buf, 0, sizeof(struct v4l2_buffer)); buf.index = i; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; if (0 != ioctl(fd, VIDIOC_QBUF, &buf)) 启动摄像头 if (0 != ioctl(fd, VIDIOC_STREAMON, &type)) 使用 poll 从 buffer 获取数据，并写入文件，然后再放回队列 while (1) { /* poll */ memset(fds, 0, sizeof(fds)); fds[0].fd = fd; fds[0].events = POLLIN; if (1 == poll(fds, 1, -1)) { /* 把buffer取出队列 */ struct v4l2_buffer buf; memset(&buf, 0, sizeof(struct v4l2_buffer)); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; if (0 != ioctl(fd, VIDIOC_DQBUF, &buf)) { perror("Unable to dequeue buffer"); return -1; } /* 把buffer的数据存为文件 */ sprintf(filename, "video_raw_data_%04d.jpg", file_cnt++); int fd_file = open(filename, O_RDWR | O_CREAT, 0666); if (fd_file < 0) { printf("can not create file : %s\n", filename); } printf("capture to %s\n", filename); write(fd_file, bufs[buf.index], buf.bytesused); close(fd_file); /* 把buffer放入队列 */ if (0 != ioctl(fd, VIDIOC_QBUF, &buf)) { perror("Unable to queue buffer"); return -1; } } } 关闭摄像头 if (0 != ioctl(fd, VIDIOC_STREAMOFF, &type)) app brightness 主线程获取图像数据，调整亮度另外开一个线程。 先用 v4l2_queryctrl V4L2_CID_BRIGHTNESS 获取 亮度范围，然后再用 VIDIOC_G_CTRL 获取当前亮度。 再用 getchar 获取按键，然后调整当前亮度数值，并用 VIDIOC_S_CTRL 来设置亮度。 v4l2 驱动框架 v4l2 接口，已经在 v4l2-dev.h 中定义好了，在 v4l2-dev.c 实现好了。 重要的结构体 struct video_device 以及其中的 const struct v4l2_file_operations *fops; 具体可以参考 drivers\media\usb\airspy\airspy.c 中的实现。 s->vdev = airspy_template; 赋值 video_device video_set_drvdata(&s->vdev, s); 把 s 赋值给 video_device 中的 device 里面的 driver_data 注册 v4l2_device s->v4l2_dev.release = airspy_video_release; ret = v4l2_device_register(&intf->dev, &s->v4l2_dev); s->vdev.v4l2_dev = &s->v4l2_dev; 把 v4l2_device 赋值给 video_device ret = video_register_device(&s->vdev, VFL_TYPE_SDR, -1); 注册 video_device vdev->cdev->ops = &v4l2_fops; 把 file_operations 赋值给 video_device v4l2 ioctl 调用流程分析 app 调用 ioctl, 就是 drivers\media\v4l2-core\v4l2-dev.c 里面的 v4l2_fops->v4l2_ioctl 调用 vdev->fops->unlocked_ioctl 这个来自 drivers\media\usb\airspy\airspy.c 的 airspy_fops->unlocked_ioctl 值是 video_ioctl2 这个函数。 video_ioctl2 video_usercopy(file, cmd, arg, __video_do_ioctl); __video_do_ioctl 调用了 info = &v4l2_ioctls[_IOC_NR(cmd)]; static struct v4l2_ioctl_info v4l2_ioctls[] = { IOCTL_INFO_FNC(VIDIOC_QUERYCAP, v4l_querycap, v4l_print_querycap, 0), IOCTL_INFO_FNC(VIDIOC_ENUM_FMT, v4l_enum_fmt, v4l_print_fmtdesc, INFO_FL_CLEAR(v4l2_fmtdesc, type)), IOCTL_INFO_FNC(VIDIOC_G_FMT, v4l_g_fmt, v4l_print_format, 0), IOCTL_INFO_FNC(VIDIOC_S_FMT, v4l_s_fmt, v4l_print_format, INFO_FL_PRIO), IOCTL_INFO_FNC(VIDIOC_REQBUFS, v4l_reqbufs, v4l_print_requestbuffers, INFO_FL_PRIO | INFO_FL_QUEUE), IOCTL_INFO_FNC(VIDIOC_QUERYBUF, v4l_querybuf, v4l_print_buffer, INFO_FL_QUEUE | INFO_FL_CLEAR(v4l2_buffer, length)), IOCTL_INFO_STD(VIDIOC_G_FBUF, vidioc_g_fbuf, v4l_print_framebuffer, 0), IOCTL_INFO_STD(VIDIOC_S_FBUF, vidioc_s_fbuf, v4l_print_framebuffer, INFO_FL_PRIO), 上面的 v4l2_ioctls 中含有 IOCTL_INFO_FNC 和 IOCTL_INFO_STD 两种。 struct v4l2_ioctl_info { unsigned int ioctl; u32 flags; const char * const name; union { u32 offset; int (*func)(const struct v4l2_ioctl_ops *ops, struct file *file, void *fh, void *p); } u; void (*debug)(const void *arg, bool write_only); }; #define IOCTL_INFO_STD(_ioctl, _vidioc, _debug, _flags) \ [_IOC_NR(_ioctl)] = { \ .ioctl = _ioctl, \ .flags = _flags | INFO_FL_STD, \ .name = #_ioctl, \ .u.offset = offsetof(struct v4l2_ioctl_ops, _vidioc), \ .debug = _debug, \ } #define IOCTL_INFO_FNC(_ioctl, _func, _debug, _flags) \ [_IOC_NR(_ioctl)] = { \ .ioctl = _ioctl, \ .flags = _flags | INFO_FL_FUNC, \ .name = #_ioctl, \ .u.func = _func, \ .debug = _debug, \ } 可以看出来，std 这种是在 struct v4l2_ioctl_ops 中的 offset 便宜，而 func 是直接用的赋值的函数。这个 std 对应的是: // drivers\media\usb\airspy\airspy.c static const struct v4l2_ioctl_ops airspy_ioctl_ops = { .vidioc_querycap = airspy_querycap, .vidioc_enum_fmt_sdr_cap = airspy_enum_fmt_sdr_cap, .vidioc_g_fmt_sdr_cap = airspy_g_fmt_sdr_cap, .vidioc_s_fmt_sdr_cap = airspy_s_fmt_sdr_cap, .vidioc_try_fmt_sdr_cap = airspy_try_fmt_sdr_cap, .vidioc_reqbufs = vb2_ioctl_reqbufs, .vidioc_create_bufs = vb2_ioctl_create_bufs, .vidioc_prepare_buf = vb2_ioctl_prepare_buf, .vidioc_querybuf = vb2_ioctl_querybuf, .vidioc_qbuf = vb2_ioctl_qbuf, .vidioc_dqbuf = vb2_ioctl_dqbuf, ... videobuffer2 结构体 airspy_ioctl_ops 中的 .vidioc_reqbufs = vb2_ioctl_reqbufs, int vb2_ioctl_reqbufs(struct file *file, void *priv, struct v4l2_requestbuffers *p) res = vb2_core_reqbufs(vdev->queue, p->memory, &p->count); allocated_buffers = __vb2_queue_alloc(q, memory, num_buffers, num_planes, plane_sizes); 在 __vb2_queue_alloc 中用到了 vb2_queue vb2_buffer 等结构体。实际的 buffer 是 vb2_queue.bufs -> vb2_buffer.planes -> vb2_plane.mem_priv -> vb2_vmalloc_buf.vddr 指向真正的 buffer. 一般申请 buffer 之后，会用 ioctl(fd, VIDIOC_QBUF, &buf) 先把 buffer 挂在 vb2_queue.queued_list, 等到摄像头保存数据之后，就把 buffer 挂到 vb2_queue.done_list , 等到 app 读取完数据后，就可以用 ioctl(fd, VIDIOC_DQBUF, &buf) 从 done_list 拿数据之后，就可以用 ioctl(fd, VIDIOC_QBUF, &buf) 再把 buffer 挂到 queued_list 中。 videobuffer2 的 ops video_device 对应了两个 ops: static struct video_device airspy_template = { .name = "AirSpy SDR", .release = video_device_release_empty, .fops = &airspy_fops, .ioctl_ops = &airspy_ioctl_ops, }; vb2_queue 对应了三个 ops: s->vb_queue.ops = &airspy_vb2_ops; s->vb_queue.mem_ops = &vb2_vmalloc_memops; ret = vb2_queue_init(&s->vb_queue); q->buf_ops = &v4l2_buf_ops; 其中: app 和 驱动之间的 open,read 等操作，靠的是 video_device 中的： static const struct v4l2_file_operations airspy_fops static const struct v4l2_file_operations airspy_fops = { .owner = THIS_MODULE, .open = v4l2_fh_open, .release = vb2_fop_release, .read = vb2_fop_read, .poll = vb2_fop_poll, .mmap = vb2_fop_mmap, .unlocked_ioctl = video_ioctl2, }; static const struct v4l2_ioctl_ops airspy_ioctl_ops static const struct v4l2_ioctl_ops airspy_ioctl_ops = { .vidioc_querycap = airspy_querycap, .vidioc_enum_fmt_sdr_cap = airspy_enum_fmt_sdr_cap, .vidioc_g_fmt_sdr_cap = airspy_g_fmt_sdr_cap, .vidioc_s_fmt_sdr_cap = airspy_s_fmt_sdr_cap, .vidioc_try_fmt_sdr_cap = airspy_try_fmt_sdr_cap, .vidioc_reqbufs = vb2_ioctl_reqbufs, .vidioc_create_bufs = vb2_ioctl_create_bufs, .vidioc_prepare_buf = vb2_ioctl_prepare_buf, .vidioc_querybuf = vb2_ioctl_querybuf, .vidioc_qbuf = vb2_ioctl_qbuf, .vidioc_dqbuf = vb2_ioctl_dqbuf, ... app 和 驱动之间 buf 也就是 v4l2_buffer 和 vb2_buffer 之间，靠的是 vb2_ops， vb2_mem_ops， vb2_buf_ops。 struct vb2_buf_ops 中重要的是 fill_user_buffer 向用户 buf提供数据，fill_vb2_buffer 向驱动 buf 提供数据 // drivers\media\v4l2-core\videobuf2-v4l2.c static const struct vb2_buf_ops v4l2_buf_ops = { .verify_planes_array = __verify_planes_array_core, .fill_user_buffer = __fill_v4l2_buffer, .fill_vb2_buffer = __fill_vb2_buffer, .copy_timestamp = __copy_timestamp, }; struct vb2_mem_ops 主要用于内存辅助函数 // drivers\media\v4l2-core\videobuf2-vmalloc.c const struct vb2_mem_ops vb2_vmalloc_memops = { .alloc = vb2_vmalloc_alloc, .put = vb2_vmalloc_put, .get_userptr = vb2_vmalloc_get_userptr, .put_userptr = vb2_vmalloc_put_userptr, #ifdef CONFIG_HAS_DMA .get_dmabuf = vb2_vmalloc_get_dmabuf, #endif .map_dmabuf = vb2_vmalloc_map_dmabuf, .unmap_dmabuf = vb2_vmalloc_unmap_dmabuf, .attach_dmabuf = vb2_vmalloc_attach_dmabuf, .detach_dmabuf = vb2_vmalloc_detach_dmabuf, .vaddr = vb2_vmalloc_vaddr, .mmap = vb2_vmalloc_mmap, .num_users = vb2_vmalloc_num_users, }; struct vb2_ops 用于硬件操作相关的处理 // drivers\media\usb\airspy\airspy.c static const struct vb2_ops airspy_vb2_ops = { .queue_setup = airspy_queue_setup, .buf_queue = airspy_buf_queue, .start_streaming = airspy_start_streaming, .stop_streaming = airspy_stop_streaming, .wait_prepare = vb2_ops_wait_prepare, .wait_finish = vb2_ops_wait_finish, }; videobuffer2 情景分析 分配内存 app 通过 ioctl(fd, VIDIOC_REQBUFS, &rb) 调用了驱动中的 vb2_ioctl_reqbufs, .vidioc_reqbufs = vb2_ioctl_reqbufs, int vb2_ioctl_reqbufs(struct file *file, void *priv, struct v4l2_requestbuffers *p) res = vb2_core_reqbufs(vdev->queue, p->memory, &p->count); ret = call_qop(q, queue_setup, q, &num_buffers, &num_planes, plane_sizes, q->alloc_devs); ... __vb2_queue_alloc(q, memory, num_buffers, num_planes, plane_sizes); ret = __vb2_buf_mem_alloc(vb); mem_priv = call_ptr_memop(vb, alloc, q->alloc_devs[plane] ? : q->dev, q->dma_attrs, size, dma_dir, q->gfp_flags); #define call_qop(q, op, args...) \ ((q)->ops->op ? (q)->ops->op(args) : 0) #define call_ptr_memop(vb, op, args...) \ ((vb)->vb2_queue->mem_ops->op ? \ (vb)->vb2_queue->mem_ops->op(args) : NULL) call_qop 宏展开: vdev->queue->ops->queue_setup 调用了 struct vb2_ops 里面的 queue_setup call_ptr_memop 宏展开: vb->vb2_queue->mem_ops->alloc 调用了 struct vb2_mem_ops 里面的 alloc buffer 放入队列 app 通过 ioctl(fd, VIDIOC_QBUF, &buf) 调用了驱动中的 vb2_ioctl_qbuf, .vidioc_qbuf = vb2_ioctl_qbuf, int vb2_ioctl_qbuf(struct file *file, void *priv, struct v4l2_buffer *p) return vb2_qbuf(vdev->queue, p); vb2_core_qbuf(q, b->index, b); ret = __buf_prepare(vb, pb); ret = __qbuf_mmap(vb, pb); if (pb) ret = call_bufop(vb->vb2_queue, fill_vb2_buffer, vb, pb, vb->planes); return ret ? ret : call_vb_qop(vb, buf_prepare, vb); list_add_tail(&vb->queued_entry, &q->queued_list); if (pb) call_void_bufop(q, copy_timestamp, vb, pb); if (q->start_streaming_called) __enqueue_in_driver(vb); for (plane = 0; plane < vb->num_planes; ++plane) call_void_memop(vb, prepare, vb->planes[plane].mem_priv); call_void_vb_qop(vb, buf_queue, vb); if (pb) call_void_bufop(q, fill_user_buffer, vb, pb); if (q->streaming && !q->start_streaming_called && q->queued_count >= q->min_buffers_needed) { ret = vb2_start_streaming(q); ... } list_for_each_entry(vb, &q->queued_list, queued_entry) __enqueue_in_driver(vb); for (plane = 0; plane < vb->num_planes; ++plane) call_void_memop(vb, prepare, vb->planes[plane].mem_priv); call_void_vb_qop(vb, buf_queue, vb); ret = call_qop(q, start_streaming, q, atomic_read(&q->owned_by_drv_count)); #define call_bufop(q, op, args...) \ ({ \ int ret = 0; \ if (q && q->buf_ops && q->buf_ops->op) \ ret = q->buf_ops->op(args); \ ret; \ }) #define call_vb_qop(vb, op, args...) \ ((vb)->vb2_queue->ops->op ? (vb)->vb2_queue->ops->op(args) : 0) #define call_void_memop(vb, op, args...) \ do { \ if ((vb)->vb2_queue->mem_ops->op) \ (vb)->vb2_queue->mem_ops->op(args); \ } while (0) #define call_void_bufop(q, op, args...) \ ({ \ if (q && q->buf_ops && q->buf_ops->op) \ q->buf_ops->op(args); \ }) #define call_qop(q, op, args...) \ ((q)->ops->op ? (q)->ops->op(args) : 0) 注: struct vb2_ops 是硬件相关的函数。 call_bufop 宏展开: vb->vb2_queue->buf_ops->fill_vb2_buffer 调用了 struct vb2_buf_ops 里面的 fill_vb2_buffer call_vb_qop 宏展开: vb->vb2_queue->ops->buf_prepare 调用了 struct vb2_ops 里面的 buf_prepare list_add_tail(&vb->queued_entry, &q->queued_list); vb2_buffer 加入到 vb2_queue call_bufop 宏展开: vb2_queue->buf_ops->copy_timestamp 调用了 struct vb2_buf_ops 里面的 copy_timestamp call_void_memop(vb, prepare, vb->planes[plane].mem_priv); 宏展开: vb->vb2_queue->mem_ops->prepare 调用了 struct vb2_mem_ops 里面的 prepare call_void_vb_qop(vb, buf_queue, vb); 宏展开: vb->vb2_queue->ops->buf_queue 调用了 struct vb2_ops 里面的 buf_queue call_void_bufop(q, fill_user_buffer, vb, pb); 宏展开: vb2_queue->buf_ops->fill_user_buffer 调用了 struct vb2_buf_ops 里面的 fill_user_buffer call_qop(q, start_streaming, q, atomic_read(&q->owned_by_drv_count)); 宏展开: vb2_queue->ops->start_streaming 调用了 struct vb2_ops 里面的 start_streaming 回顾与编写驱动框架 drivers\media\v4l2-core\v4l2-dev.c 中的 fops 是 file_operations 经过一路调用，最后到硬件相关的 v4l2_file_operations // drivers\media\v4l2-core\v4l2-dev.c static const struct file_operations v4l2_fops = { .owner = THIS_MODULE, .read = v4l2_read, .write = v4l2_write, .open = v4l2_open, .get_unmapped_area = v4l2_get_unmapped_area, .mmap = v4l2_mmap, .unlocked_ioctl = v4l2_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = v4l2_compat_ioctl32, #endif .release = v4l2_release, .poll = v4l2_poll, .llseek = no_llseek, }; // drivers\media\usb\airspy\airspy.c static const struct v4l2_file_operations airspy_fops = { .owner = THIS_MODULE, .open = v4l2_fh_open, .release = vb2_fop_release, .read = vb2_fop_read, .poll = vb2_fop_poll, .mmap = vb2_fop_mmap, .unlocked_ioctl = video_ioctl2, }; 有两个 device，通过 g_vdev.v4l2_dev = &g_v4l2_dev; 这样的方法，把 v4l2_device 赋值给 video_device 中的一个成员变量。注册的时候，先注册 v4l2_device 后注册 video_device ; 卸载 v4l2_device 需要先 s->v4l2_dev.release = airspy_video_release; 设置好 release，然后让内核自动调用。 一个是 struct video_device, 用 ret = video_register_device(&g_vdev, VFL_TYPE_SDR, -1); 注册 一个是 struct v4l2_device , 用 ret = v4l2_device_register(NULL, &g_v4l2_dev); 注册。 虚拟摄像头 ioctl 根据 app 前面使用到的 ioctl 来实现驱动中相关的函数。 ioctl(fd, VIDIOC_QUERYCAP, &cap) static const struct v4l2_ioctl_ops virtual_ioctl_ops 增加 .vidioc_querycap = virtual_querycap, 并在函数中配置 struct v4l2_capability ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) static const struct v4l2_ioctl_ops virtual_ioctl_ops 增加 .vidioc_enum_fmt_vid_cap = virtual_enum_fmt_cap, 并在函数中配置 f->pixelformat = V4L2_PIX_FMT_MJPEG;，用于向 app 返回可用格式。 ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &fsenum) static const struct v4l2_ioctl_ops virtual_ioctl_ops 增加 .vidioc_enum_framesizes = virtual_enum_framesizes, 并在函数中配置 分辨率, 用于向 app 返回可用分辨率 ioctl(fd, VIDIOC_S_FMT, &fmt) static const struct v4l2_ioctl_ops virtual_ioctl_ops 增加 .vidioc_s_fmt_vid_cap = virtual_s_fmt_cap, 并在函数中配置从 app 传过来的参数，如果传过来的参数不可用，就返回可用参数。 其他 ioctl 其他 ioctl 中可以在 static const struct v4l2_ioctl_ops virtual_ioctl_ops 中配置为内核提供的 vb2_ioctl_ 开头的函数。 驱动 buff 代码 buffer 相关的三个结构体中，vb2_buf_ops 和 vb2_mem_ops 使用内核自带的函数，vb2_ops 使用的是硬件相关，需要开发人员提供函数。 static const struct vb2_ops virtul_vb2_ops = { .queue_setup = virtual_queue_setup, .buf_queue = virtual_buf_queue, .start_streaming = virtual_start_streaming, .stop_streaming = virtual_stop_streaming, .wait_prepare = vb2_ops_wait_prepare, .wait_finish = vb2_ops_wait_finish, }; queue_setup 用于检查用户需求的 buf 数量是否够用，驱动程序可以调整数据。并且把有些地址进行页面对其。 buf_queue 通过 container_of 从 vb2_buffer 找到 vb2_v4l2_buffer 再找到含有 list 的 airspy_frame_buf 通过 list_add_tail 把 airspy_frame_buf 挂载到 设备的 queued_bufs 下面。 驱动 传输相关 在 start_streaming 之后，会启动数据传输，然后在相关的中断函数中，去读取摄像头数据，并保存到 buffer 中，然后放到 done 的列表中，供 app 使用。 start_streaming 简单的设置并开始一个定时器，然后在定时器中进行具体处理。 virtual_timer_expire fbuf = airspy_get_next_fill_buf(s); 先实现这个函数，找到下一个可用的 buffer ptr = vb2_plane_vaddr(&fbuf->vb.vb2_buf, 0); 从 下一个可用的 buffer 获得 写入地址。 len = airspy_convert_stream(s, ptr, urb->transfer_buffer, urb->actual_length); 把具体数据写入到 buffer 中。 vb2_set_plane_payload(&fbuf->vb.vb2_buf, 0, len); 设置前面写入 buffer 的长度。 vb2_buffer_done(&fbuf->vb.vb2_buf, VB2_BUF_STATE_DONE); 把 buffer 放到 done list 中。 驱动 上级调试 可以参考 drivers\media\usb\airspy\airspy.c drivers\media\usb\uvc\uvc_driver.c 相关的 lock 还是需要提供，在 prepare 之类的时候，会被内核中其他函数使用。 ret = video_register_device(&g_vdev, VFL_TYPE_GRABBER, -1); 参数建议用 VFL_TYPE_GRABBER，创建设备的时候，是 /dev/video*, 如果是 VFL_TYPE_SDR，设备名称就是一个 sw 开头的。 v4l2_ioctl_ops 中的 vidioc_g_fmt_vid_cap 函数也需要实现，用于获取当前摄像头的 v4l2_pix_format stop_streaming 的实现函数中，需要把 queue 链表中的所有 buffer，全部通过 vb2_buffer_done(&buf->vb.vb2_buf, VB2_BUF_STATE_ERROR); 这个操作一边，才能正常退出。 注: 可以在 app 中使用 perror 来打印出更多的信息。
总结 usb 框架 usb 每一级都可以是 hub 或者 设备，但是 hub 最多6级，第7级只能是设备。 app 可以通过两种方法来访问硬件 app -> usb device driver -> usb host driver -> host 硬件 -> device 硬件 app -> libusb -> usb host driver -> host 硬件 -> device 硬件 电气信号 低速，d-变化，全速和高速 d+ 变化， 先识别到全速，然后 host 会发送一个复位，device 复位之后，会和 host 之间发送 K J 信号，如果没问题，那就是高速 host + 高速设备，然后高速设备就会取消 d+ 的上拉电阻。 高速时， host 和 设备都会下拉电阻用于防止信号反射，一旦 设备拔出，信号反射变化变大，就可以被检测出来。 从 idle 的 j 状态变化为 k 状态，就是 sop 信号了，后面就是要开始传输信号了。 NRZI：反向不归零编码，波形变化表示0，波形不变表示1. 当连续传输6个1之后，必须强制插入一个0，防止双方在长期不变信号的情况下，传输错位。 协议，数据包格式 外层包：sop + sync + 包内容 + eop 包内容: pid + 地址 + 帧号 + 数据 + crc pid: 方向 + 类型。 一个完整的事务，含有 令牌包 + 数据包 + 握手回应包，对应了 令牌域，数据域，握手域。 host 写，含有 host 发送的 令牌包（地址） + 数据包，以及 device 发送的 握手回应包。 host 读，含有 host 发送的 令牌包（地址） + device 发送的数据包，以及 host 发送的 握手回应包。 数据传输形式 批量传输：u盘，可靠，非实时 中断传输：鼠标，可靠，实时 实时传输：摄像头，非可靠，实时 控制传输：识别、枚举等。 控制传输由多个事务实现，其他传输由一个事务实现。 批量传输：读和写都是完整的事务，另外还有一个 ping 查询包 中断传输: 读和写都是完整事务，但是要周期性的传输。 实时传输: 没有握手包。 控制传输：由设置阶段 + 数据阶段 + 状态阶段组成 设置阶段: 令牌包中指明 setup 数据阶段: 由一个或多个批量传输来组成 状态阶段：由一个批量传输组成，如果数据阶段是读或写，那么本阶段会反向；如果无数据阶段，那么本阶段就是读 体验数据格式 usbprotocolsuit 中的示例程序可以看数据格式。 高速模式，sync 4个字节，eop 1个字节，字节长度是包含他们的。 描述符 一个硬件设备可能有多种配置，一般情况下，只能生效一种配置。 比如：上网卡有 u盘 和 上网两种配置。 一个配置下，可能有多个接口也就是功能逻辑设备，一般可以同时生效。比如，耳机有音频和按键 一个接口，可能有多个端点，一种端点只能由一种数据方向。 比如说音频接口，有播放和录音这两个端点。 所以描述符会有：设备描述符，配置描述符，接口描述符，端点描述符。 usb设备，都有端口0，用于最开始的通讯。 linux 上面可以使用 lsusb -v 来查看当前的 usb 描述符。 枚举过程 全速设备在复位后，会检查是否支持高速。 设备初始地址为0， host 会和这个地址 0, ep0 进行通讯。设置地址之类的事情 启用新地址后， host 与新地址通信。 配置可用配置 在设置事务中，数据阶段，都是标准的 8字节数据，并且是固定格式。 枚举流程: 获取设备描述符 设置地址 获取设备描述符 获取配置描述符 设置配置 libusb app 可以通过 libusb 来和 host 驱动打交道。 但是这就要求 app 需要阅读 usb 设备手册，了解设备相关细节，然后才能用 libusb 提供的接口进行操作。 本来这些 usb 设备相关的工作，应该由驱动工程师来实现。 app 有两种方式来打开 usb 设备 知道确切的 usb 编号，就可以通过编号直接打开 不知道编号，就需要遍历所有 sub 设备，然后查看设备描述符和配置描述符，确定好之后，再打开。 app 通过 libusb 使用 usb 设备的时候，还需要通过 detach 相关函数，先移除驱动，然后再 claim 函数，认领这个 usb 设备。 传输可以通过 同步传输也可以异步传输。 另外 libusb 使用的时候，也需要 init, open, detach claim 等操作以及相应的反操作。 libusb api libusb 异步的好处是，可以针对多 ep 进行操作，并且可以取消传输。 同步只能对一个 ep 进行操作，除非用多线程。 异步传输主要是几个步骤: 分配 填充 提交 处理事件，检查结果，回调函数。 释放 usb 鼠标 drivers\hid\usbhid\usbmouse.c linux 鼠标驱动 上报数据，不同鼠标不太一样，但是大体的顺序是一样的： bit0: 左键 bit[1]: 右键 bit[2]: 中键 bit[3]: 侧键 bit[4]: 额外键 libusb 鼠标 可以参考 libusb-1.0.26 自带的 example dpfp.c 去编写. 也可以参考 openocd 相关的 cmsis_dap_usb_bulk.c 整体流程: init err = libusb_init(NULL); get device list if ((num_devices = libusb_get_device_list(NULL, &devs)) < 0) for each device, get config descriptor for (int i = 0; i < num_devices; i++) { dev = devs[i]; /* parse interface descriptor, find usb mouse */ err = libusb_get_config_descriptor(dev, 0, &config_desc); 遍历 interface for (int interface = 0; interface < config_desc->bNumInterfaces; interface++) { const struct libusb_interface_descriptor *intf_desc = &config_desc->interface[interface].altsetting[0]; interface_num = intf_desc->bInterfaceNumber; 找到鼠标条件 if (intf_desc->bInterfaceClass != 3 || intf_desc->bInterfaceProtocol != 2) else 找到了输入的中断端点 for (int ep = 0; ep < intf_desc->bNumEndpoints; ep++) { if ((intf_desc->endpoint[ep].bmAttributes & 3) == LIBUSB_TRANSFER_TYPE_INTERRUPT || (intf_desc->endpoint[ep].bEndpointAddress & 0x80) == LIBUSB_ENDPOINT_IN) { /* 找到了输入的中断端点 */ fprintf(stdout, "find in int endpoint\n"); endpoint = intf_desc->endpoint[ep].bEndpointAddress; found = 1; break; } } 释放 config libusb_free_config_descriptor(config_desc); 如果找到鼠标，就 err = libusb_open(dev, &dev_handle);， 如果没找到需要 libusb_free_device_list(devs, 1); libusb_exit(NULL); free device list libusb_free_device_list(devs, 1); claim interface libusb_set_auto_detach_kernel_driver(dev_handle, 1); err = libusb_claim_interface(dev_handle, interface_num); libusb_interrupt_transfer while (1) { err = libusb_interrupt_transfer(dev_handle, endpoint, buffer, 16, &transferred, 5000); libusb_close libusb_release_interface(dev_handle, interface_num); libusb_close(dev_handle); libusb_exit(NULL); 编译 本机编译时需要 libusb 1.0 的开发包，需要安装 libusb-1.0-0-dev, 还有链接的时候，也需要 -lusb-1.0 交叉编译，需要安装 libtool libudev 交叉编译libusb sudo apt-get install libtool unzip libusb-1.0.26.zip cd libusb-1.0.26 ./autogen.sh ./configure --host=arm-buildroot-linux-gnueabihf --prefix=$PWD/tmp make make install ls tmp/ include lib 安装库、头文件到工具链的目录里 libusb-1.0.26/tmp/lib$ cp * -rfd /home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/arm-buildroot-linux-gnueabihf/sysroot/usr/lib/ libusb-1.0.26/tmp/include$ cp libusb-1.0 -rf /home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/arm-buildroot-linux-gnueabihf/sysroot/usr/include/ 交叉编译app arm-buildroot-linux-gnueabihf-gcc -o readmouse.c -lusb-1.0 libusb 鼠标 异步 异步情况下，可以对多个 ep 进行处理，所以可以用链表和结构体来操作。 struct usb_mouse { struct libusb_device_handle *handle; int interface; int endpoint; unsigned char buf[16]; int transferred; struct libusb_transfer *transfer; struct usb_mouse *next; }; 找到鼠标后处理 err = libusb_open(dev, &dev_handle); pmouse = malloc(sizeof(struct usb_mouse)); pmouse->endpoint = endpoint; pmouse->interface = interface_num; pmouse->handle = dev_handle; pmouse->next = NULL; if (!list) list = pmouse; else { pmouse->next = list; list = pmouse; } claim interface pmouse = usb_mouse_list; while (pmouse) { libusb_set_auto_detach_kernel_driver(pmouse->handle, 1); err = libusb_claim_interface(pmouse->handle, pmouse->interface); pmouse = pmouse->next; } alloc transfer, fill transfer, submit transfer pmouse = usb_mouse_list; while (pmouse) { /* alloc transfer */ pmouse->transfer = libusb_alloc_transfer(0); /* fill transfer */ libusb_fill_interrupt_transfer(pmouse->transfer, pmouse->handle, pmouse->endpoint, pmouse->buf, sizeof(pmouse->buf), mouse_irq, pmouse, 0); /* submit transfer */ libusb_submit_transfer(pmouse->transfer); pmouse = pmouse->next; } mouse_irq static void mouse_irq(struct libusb_transfer *transfer) { static int count = 0; if (transfer->status == LIBUSB_TRANSFER_COMPLETED) { /* parser data */ printf("%04d datas: ", count++); for (int i = 0; i < transfer->actual_length; i++) { printf("%02x ", transfer->buffer[i]); } printf("\n"); } if (libusb_submit_transfer(transfer) < 0) { fprintf(stderr, "libusb_submit_transfer err\n"); } } handle events 用于处理回调函数 while (1) { struct timeval tv = { 5, 0 }; int r; r = libusb_handle_events_timeout(NULL, &tv); } libusb_close pmouse = usb_mouse_list; while (pmouse) { libusb_release_interface(pmouse->handle, pmouse->interface); libusb_close(pmouse->handle); pmouse = pmouse->next; } 列表清空 void free_usb_mouses(struct usb_mouse *usb_mouse_list) { struct usb_mouse *pnext; while (usb_mouse_list) { pnext = usb_mouse_list->next; free(usb_mouse_list); usb_mouse_list = pnext; } } usb 设备驱动模型 也是和 i2c, spi 一样，usb 也有自己的总线，一边是驱动，另外一边是 usb设备的 interface. 具体可以参考 drivers\hid\usbhid\usbmouse.c include\linux\usb.h 里面是可以使用的usb 函数，这些函数的对象是 usb 设备里面的某个 ep, 成为 pipe. 同步传输，可以直接用相关函数 异步传输，需要先分配设置提交一个 URB(usb request block), 当传输完成后，回调函数会被调用。 一般传输中，在提交 URB 的时候，会临时分配一个 DMA buffer, 当然可以用 usb_alloc_coherent 来搞出来 DMA buffer。 usb 设备驱动框架 在 probe 中，构造注册一个 input_dev, 获得数据通过构造提交 URB，并在回调函数中向 input 系统上报数据。 usb 设备驱动 分配 input input_dev = devm_input_allocate_device(&intf->dev); input 上报需要拿到 usb 相关的信息，所以这些需要设置为 input 的数据，方便后续使用 struct usb_mouse_as_key_desc { struct usb_device *dev; struct usb_interface *intf; const struct usb_device_id *id; int pipe, maxp; int bInterval; void *data_buffer; dma_addr_t data_dma; struct urb *urb; }; desc = kmalloc(sizeof(struct usb_mouse_as_key_desc), GFP_KERNEL); desc->dev = dev; desc->intf = intf; desc->id = id; desc->pipe = pipe; desc->maxp = maxp; desc->bInterval = endpoint->bInterval; desc->data_buffer = usb_alloc_coherent(dev, maxp, GFP_ATOMIC, &desc->data_dma); input_set_drvdata(input_dev, desc); input 设置事件 /* set 1: which type event ? */ __set_bit(EV_KEY, input_dev->evbit); /* set 2: which event ? */ __set_bit(KEY_L, input_dev->keybit); __set_bit(KEY_S, input_dev->keybit); __set_bit(KEY_ENTER, input_dev->keybit); usb 传输，只有在 input 被 open 之后才工作 input_dev->open = usb_mouse_as_key_open; input_dev->close = usb_mouse_as_key_close; 注册 input error = input_register_device(input_dev); input 也作为 usb 数据 usb_set_intfdata(intf, input_dev); open 中，分配设置 URB, 其中不变的 pipe, dev 等放在 probe 中。 static int usb_mouse_as_key_open(struct input_dev *dev) { struct urb *urb; struct usb_mouse_as_key_desc *desc = input_get_drvdata(dev); int err; printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__); /* 分配/填充/提交 URB */ urb = usb_alloc_urb(0, GFP_KERNEL); desc->urb = urb; usb_fill_int_urb(urb, desc->dev, desc->pipe, desc->data_buffer, (desc->maxp > 8 ? 8 : desc->maxp), usb_mouse_as_key_irq, dev, desc->bInterval); urb->transfer_dma = desc->data_dma; urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; err = usb_submit_urb (urb, GFP_ATOMIC); printk("%s %s %d, err = %d\n", __FILE__, __FUNCTION__, __LINE__, err); return err; } 按照 usbmouse.c，设置 pipe等。 interface = intf->cur_altsetting; if (interface->desc.bNumEndpoints != 1) return -ENODEV; endpoint = &interface->endpoint[0].desc; if (!usb_endpoint_is_int_in(endpoint)) return -ENODEV; pipe = usb_rcvintpipe(dev, endpoint->bEndpointAddress); maxp = usb_maxpacket(dev, pipe, usb_pipeout(pipe)); close static void usb_mouse_as_key_close(struct input_dev *dev) { struct usb_mouse_as_key_desc *desc = input_get_drvdata(dev); /* 取消/释放 URB */ usb_kill_urb(desc->urb); usb_free_urb(desc->urb); } urb irq 中，通过 input 上报数据 static void usb_mouse_as_key_irq(struct urb *urb) { struct input_dev *dev = urb->context; struct usb_mouse_as_key_desc *desc = input_get_drvdata(dev); signed char *data = desc->data_buffer; int status; switch (urb->status) { case 0: /* success */ break; case -ECONNRESET: /* unlink */ case -ENOENT: case -ESHUTDOWN: return; /* -EPIPE: should clear the halt */ default: /* error */ goto resubmit; } input_report_key(dev, KEY_L, data[1] & 0x01); input_report_key(dev, KEY_S, data[1] & 0x02); input_report_key(dev, KEY_ENTER, data[1] & 0x04); input_sync(dev); resubmit: status = usb_submit_urb (urb, GFP_ATOMIC); } usb设备拔出 static void usb_mouse_as_key_disconnect(struct usb_interface *intf) { struct input_dev *input_dev = usb_get_intfdata (intf); struct usb_mouse_as_key_desc *desc = input_get_drvdata(input_dev); usb_free_coherent(desc->dev, desc->maxp, desc->data_buffer, desc->data_dma); kfree(desc); input_unregister_device(input_dev); usb_set_intfdata(intf, NULL); } usb 设备调试 别忘了，把内核中已经有的 usb_hid 给去掉。否则会被这个驱动先接管。 usb 鼠标，支持两种通信协议， 一种简单的是 boot protocol, usbmouse.c 用的就是这种 另外一种复杂的是 report protocol, hid-core.c 用的是这种 一般鼠标默认用的是 report protocol，如果想要修改，需要用控制请求 Set_Protocol 来修改。 otg 通过 id 引脚，确认自己是否是 host，如果是 host，就对电源引脚供电。
总结 整体 spi 分为 soc 上面的 spi master 和 外接的 spi device. 在设备树中，spi 节点本身是 spi master, 子节点就是对应的 spi device. 加载驱动的时候，根据 compitble 识别到 spi master, 并作为 platform 总线设备，匹配到对应的总线驱动。 在其 probe 中，注册 spi 总线master 并识别设备树中的 spi 子节点，并生成 spi 总线设备，并匹配到 spi 总线驱动。 设备树 可以参考内核文档 Documentation\devicetree\bindings\spi\spi-bus.txt, 有详细的说明。 deivce 必须提供的三个内容是，compatible, frequecy, reg. reg 用于指明是 cs-gpios 中的第几个。 master 必须提供的内容是: address-cells：这个SPI Master下的SPI设备，需要多少个cell来表述它的片选引脚 size-cells：必须设置为0 compatible：根据它找到SPI Master驱动 cs-gpios：SPI Master可以使用多个GPIO当做片选，可以在这个属性列出那些GPIO num-cs：片选引脚总数 驱动 spidev.c 中的 init 可以看出，并没有直接创建 device, 而是会直接注册 spi device 驱动。 在这个驱动的 probe 中，会根据设备树的情况，来创建 device，并把私有数据结构，挂到spi 链表上，用于后续的 open 来找到私有数据结构。 spidev.c 提供的 read, write 只能用于写或读，ioctrl 只能用与写一个字节后读，如果想要实现复杂功能，需要自己编写驱动。 内核中提供了相关的测试程序，在 tools\spi\spidev_fdx.c 里面。 spi设备使用 设备树 设备树中，compatible = "spidev" 这个会有点警告，不过可以用，因为并不是匹配 of_match_table. 具体的匹配在 spi.c 中的 spi_match_device 函数中 strcmp(spi->modalias, drv->name) 。 内核 内核中的 spidev 需要启用，名称是spi 名目下面的 user xxx. gpio 控制 gpio 控制一般可以三种方式， sysfs 接口：简单易用，适合快速测试 GPIO 功能。 类似于命令行方式。 libgpiod 库：高效灵活，推荐在生产环境中使用。 需要安装 libgpiod 库 mmap 直接访问硬件：适合高级用户，需要了解硬件寄存器布局。需要知道寄存器地址，比较麻烦。 sysfs 接口使用方式: // echo 509 > /sys/class/gpio/export char cmd[100]; dc_pin_num = number; sprintf(cmd, "echo %d > /sys/class/gpio/export", number); system(cmd); spi 使用 sysfs 控制 dc 引脚，用于数据传输，效率很慢。 自己的 spi 设备驱动 发送数据，首先需要构造 spi_message 作为链表头，然后需要收发多少次数据就构造多少个 spi_transfer，并把 spi_transfer 放入到 spi_message 的链表中。然后使用 spidev_sync 来发送。 如果只发或者只收，可以针对应的在 spi_transfer 中把 rx_buf 或者 tx_buf 设置为 null 自己的设备驱动，可以在 probe 中，直接 register_chrdev class_create device_create 一起完成。 自己的 oled spi 设备驱动 dc 使用 gpiod 放在驱动中控制，app 通过 ioctl 进行初始化或设置位置等操作，写入通过 write. framebuffer 改造 oled 正常的 qt 等程序，使用 framebuffer，像素是横向对应到 fb 里面的字节，app 并不关心具体的控制。 所以需要改造我们的驱动。把以前的 framebuffer 中的 fb_ops 相关的操作移植过来。 都处理好之后，register_framebuffer 注册 fb. 因为 app 只需要把显示的内容放到 fb 中即可。这就需要驱动定期的把 fb 内容更新到屏幕上面，可以使用 timer 或者内核线程来做这个事情。如果用内核线程，那就用 kthread_run 创建，用 schedule_timeout_interruptible 休眠。 dma 传输用到的 buffer，最好是 malloc 出来的，直接全局变量会有一定问题。 spi master spi device 传输的时候，需要 message 和 transfer。 本质上是创建 message 之后，挂到了 master 的 message 列表中，由 master 来 启动 work 挨个处理传输。 在 work 中，会先从 master 的 message 列表中取出数据，然后启动传输，并休眠等待。 等到终端唤醒这个 work，才会继续向下，唤醒 spi device. __spi_sync 传输的时候，深层有两种调用，master->transfer == spi_queued_transfer 设置了这个是新方法，就会帮你管理 queue，否则就是老方法，要自己管理 queue 并触发传输。 spi master 老方法 probe 中，先 spi_alloc_master 分配, 然后 配置 master->transfer 传输函数 和 master->dev.of_node 节点，再 INIT_WORK 创建传输用的工作队列，最后 spi_register_master 注册。 如果没有配置 master->dev.of_node 在 sysfs 中会缺少相应的节点信息。 transfer 传输函数，先配置 mesg, 再用 list_add_tail 放入消息队列，最后 schedule_work 启动工作队列。 工作队列函数，轮询检查 list_empty, 用 list_entry 取出 mesg, list_del_init 清除list节点，最后再清零 mesg->status, 并 mesg->complete 唤醒。 spi master 新方法 probe 中，先 spi_alloc_master 分配, 再 spi_master_get_devdata 获取 bitbang 然后配置 bitbang, 最后spi_bitbang_start 启动，还有别忘了 init_completion 初始化一下。 bitbang->txrx_bufs 这个对应的传输函数需要实现，先 reinit_completion 初始化，再启动传输，最后 wait_for_completion_timeout 等待传输完成。 在中断处理函数中，complete(&g_xfer_done) 完成并唤醒。 bitbang->chipselect 这个也要配置下。
总结 pci 和 pcie 都是对地址进行转换，转换情况需要上电后，写到配置寄存器中。一般这种中间设备成为 pci 桥，相关驱动是 pci_host. pci 通过地址线的方式，来选通桥后的设备。 pcie桥本身内部端口有地址，端口后只接一个设备，选通的其实是端口。 针对通过本级 pci/pcie 能够直达的，配置是，发送 type0包，不能直达的，发送 type1包。 type1包在桥之间转发，直到下级就是目标终端，才会转换为 type0 包。 数据包中，一般包含 bus, device, function, register 四部分。 桥设备会根据直接连接的下级设备，向上级索要地址范围。
总结 分层 主要分为三层， 最下面的厂商硬件相关驱动 drivers\tty\serial\imx.c 中间的行规层 drivers\tty\n_tty.c, 用于决定换行等特殊字符是否生效 上层 tty drivers\tty\tty_io.c 结构体 uart 驱动中关键结构体 uart_driver 定义在include\linux\serial_core.h，里面有两个成员变量 struct uart_state *state; struct tty_driver *tty_driver; serial_core.h 中的 struct uart_state *state; -> struct uart_port *uart_port; -> const struct uart_ops *ops; . 而在厂商驱动成员中，定义了 static const struct uart_ops imx_pops 里面包括了各种硬件相关的操作函数，probe 的时候，会把厂商的 uart_ops 给到 uart_driver一路向下的 uart_ops，让后续系统调用中可以正确的操作硬件。 include\linux\tty_driver.h 中的 struct tty_driver 包含了面向上层用的 tty 相关的内容，包括 struct cdev **cdevs; struct tty_struct **ttys; struct tty_port **ports; struct ktermios **termios; 其中 struct tty_port **ports; 指向前面 struct uart_state *state; 中的 struct tty_port port; 注册 使用 int uart_add_one_port(struct uart_driver *drv, struct uart_port *uport) 去注册。 console console 是 tty 中的一种，可以通过设备树或者 cmdline 进行配置，具体的实现也是在厂商驱动中，需要实现 static struct console imx_console 这样的结构体。
异常与中断的概念及处理流程 异常向量表 _start: b reset ldr pc, _undefined_instruction ldr pc, _software_interrupt ldr pc, _prefetch_abort ldr pc, _data_abort ldr pc, _not_used ldr pc, _irq //发生中断时，CPU跳到这个地址执行该指令 **假设地址为0x18** ldr pc, _fiq 进程线程中断的核心_栈 CPU内部的寄存器很重要，如果要暂停一个程序，中断一个程序，就需要把这些寄存器的值保存下来：这就称为保存现场。 保存在内存中，这块内存就称之为栈。程序要继续执行，就先从栈中恢复那些CPU内部寄存器的值。 并不局限于中断，包括: 函数调用 中断处理 进程切换 Linux系统对中断处理的演进 Linux对中断的扩展：硬件中断、软件中断 Linux系统把中断的意义扩展了，对于按键中断等硬件产生的中断，称之为“硬件中断”(hard irq)。每个硬件中断都有对应的处理函数，比如按键中断、网卡中断的处理函数肯定不一样。 相对的，还可以人为地制造中断：软件中断(soft irq) 有哪些软件中断？查内核源码include/linux/interrupt.h enum { HI_SOFTIRQ=0, TIMER_SOFTIRQ, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, BLOCK_SOFTIRQ, IRQ_POLL_SOFTIRQ, TASKLET_SOFTIRQ, SCHED_SOFTIRQ, HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the numbering. Sigh! */ RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */ NR_SOFTIRQS }; + 怎么触发软件中断？最核心的函数是`raise_softirq`，简单地理解就是设置`softirq_veq[nr]`的标记位 `extern void raise_softirq(unsigned int nr);` + 怎么设置软件中断的处理函数：`extern void open_softirq(int nr, void (*action)(struct softirq_action *));` + 后面讲到的中断下半部tasklet就是使用软件中断实现的 中断处理原则1：不能嵌套 如果中断嵌套突然暴发，那么栈将越来越大，栈终将耗尽。 中断处理原则2：越快越好 在单芯片系统中，假设中断处理很慢，那应用程序在这段时间内就无法执行：系统显得很迟顿。 在SMP系统中，假设中断处理很慢，那么正在处理这个中断的CPU上的其他线程也无法执行。 在中断的处理过程中，该CPU是不能进行进程调度的，所以中断的处理要越快越好，尽早让其他中断能被处理──进程调度靠定时器中断来实现。 在Linux系统中使用中断是挺简单的，为某个中断irq注册中断处理函数handler，可以使用request_irq函数, 在handler函数中，代码尽可能高效。 static inline int __must_check request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev) 要处理的事情实在太多，拆分为：上半部、下半部 在handler函数里只做紧急的事，然后就重新开中断，让系统得以正常运行；那些不紧急的事，以后再处理，处理时是开中断的。 中断下半部的实现有很多种方法，讲2种主要的：tasklet(小任务)、work queue(工作队列)。 下半部要做的事情耗时不是太长：tasklet 当下半部比较耗时但是能忍受，并且它的处理比较简单时，可以用tasklet来处理下半部。tasklet是使用软件中断来实现。 写字太多，不如贴代码，代码一目了然： 使用流程图简化一下： 简单说： 下半部，开中断的时候，可以被中断，但是被中断前，count >= 1 再次中断上半部结束的时候，会检查 count 的值，正常情况 count 是0，但是如果是再下半部中被中断的，那么 count 就不是0，就会直接退出结束本次中断 cpu 会回到上次下半部被中断的地方继续执行下去。 注意：从上面的流程可以看出，上半部和下半部的关系是 N:1. 所以，下半部处理的时候要完整一点。 比如按键处理，那就要处理所有按键。 下半部要做的事情太多并且很复杂：工作队列 如果中断要做的事情实在太耗时，那就不能用软件中断来做，而应该用内核线程来做：在中断上半部唤醒内核线程。内核线程和APP都一样竞争执行，APP有机会执行，系统不会卡顿。 这个内核线程是系统帮我们创建的，一般是kworker线程，内核中有很多这样的线程 ps -A | grep kworker, kworker线程要去“工作队列”(work queue)上取出一个一个“工作”(work)，来执行它里面的函数。 创建work：#define DECLARE_WORK(n, f) struct work_struct n = __WORK_INITIALIZER(n, f) 先写出一个函数，然后用这个宏填充一个work结构体。 要执行这个函数时，把work提交给work queue就可以了 static inline bool schedule_work(struct work_struct *work) 上述函数会把work提供给系统默认的work queue：system_wq，它是一个队列。 谁来执行work中的函数？schedule_work函数不仅仅是把work放入队列，还会把kworker线程唤醒。此线程抢到时间运行时，它就会从队列中取出work，执行里面的函数。 谁把work提交给work queue？在中断场景中，可以在中断上半部调用schedule_work函数。 新技术：threaded irq 新技术threaded irq，为每一个中断都创建一个内核线程；多个中断的内核线程可以分配到多个CPU上执行，这提高了效率。 你可以只提供thread_fn，系统会为这个函数创建一个内核线程。发生中断时，内核线程就会执行这个函数。 int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id) 下半部总结 为什么要有这么多种下半部处理方法？ tasklet： 比上半部处理的好处是，可以被中断，用于处理稍微需要点时间的事务。 本质上还是在中断处理。 work_queue： 比 tasklet 的好处是，work_queue 运行在进程上下文，可以进行休眠、阻塞、发生调度等。在内核线程中处理。 threaded_irq： 比 work_queue 的好处是，在多核处理器上运行，效率高，因为每个 threaded_irq 创建一个内核线程，所以可以被多核分别调度。 而 work_queue 只能挤在一个核上面。 Linux中断系统中的重要数据结构 最核心的结构体是irq_desc，之前为了易于理解，我们说在Linux内核中有一个中断数组，对于每一个硬件中断，都有一个数组项，这个数组就是irq_desc数组。 注意：如果内核配置了CONFIG_SPARSE_IRQ，那么它就会用基数树(radix tree)来代替irq_desc数组。SPARSE的意思是“稀疏”，假设大小为1000的数组中只用到2个数组项，那不是浪费嘛？所以在中断比较“稀疏”的情况下可以用基数树来代替数组。 irq_desc数组 irq_desc结构体在include/linux/irqdesc.h 中定义，主要内容如下图, 每一个irq_desc数组项中都有一个函数：handle_irq，还有一个action链表。 struct irq_desc { ... struct irq_data irq_data; irq_flow_handler_t handle_irq; struct irqaction *action; /* IRQ action list */ const char *name; ... } ____cacheline_internodealigned_in_smp; 外部设备1、外部设备n共享一个GPIO中断B，多个GPIO中断汇聚到GIC(通用中断控制器)的A号中断，GIC再去中断CPU。那么软件处理时就是反过来，先读取GIC获得中断号A，再细分出GPIO中断B，最后判断是哪一个外部芯片发生了中断。 中断的处理函数来源有三： GIC的处理函数 假设irq_desc[A].handle_irq是XXX_gpio_irq_handler(XXX指厂家)，这个函数需要读取芯片的GPIO控制器，细分发生的是哪一个GPIO中断(假设是B)，再去调用irq_desc[B]. handle_irq。 注意：irq_desc[A].handle_irq细分出中断后B，调用对应的irq_desc[B].handle_irq。显然中断A是CPU感受到的顶层的中断，GIC中断CPU时，CPU读取GIC状态得到中断A。 模块的中断处理函数 比如对于GPIO模块向GIC发出的中断B，它的处理函数是irq_desc[B].handle_irq。 BSP开发人员会设置对应的处理函数，一般是handle_level_irq或handle_edge_irq，从名字上看是用来处理电平触发的中断、边沿触发的中断。 注意：导致GPIO中断B发生的原因很多，可能是外部设备1，可能是外部设备n，可能只是某一个设备，也可能是多个设备。所以irq_desc[B].handle_irq会调用某个链表里的函数，这些函数由外部设备提供。这些函数自行判断该中断是否自己产生，若是则处理。 外部设备提供的处理函数： 这里说的“外部设备”可能是芯片，也可能总是简单的按键。它们的处理函数由自己驱动程序提供，这是最熟悉这个设备的“人”：它知道如何判断设备是否发生了中断，如何处理中断。 对于共享中断，比如GPIO中断B，它的中断来源可能有多个，每个中断源对应一个中断处理函数。所以irq_desc[B]中应该有一个链表，存放着多个中断源的处理函数。 一旦程序确定发生了GPIO中断B，那么就会从链表里把那些函数取出来，一一执行。这个链表就是action链表。 irqaction结构体 irqaction结构体在 include/linux/interrupt.h 中定义，主要内容如下图： struct irqaction { irq_handler_t handler; void *dev_id; struct irqaction *next; irq_handler_t thread_fn; struct task_struct *thread; struct irqaction *secondary; unsigned int irq; unsigned int flags; const char *name; ... } ____cacheline_internodealigned_in_smp; 当调用request_irq、request_threaded_irq注册中断处理函数时，内核就会构造一个irqaction结构体。在里面保存name、dev_id等，最重要的是handler、thread_fn、thread。 handler是中断处理的上半部函数，用来处理紧急的事情。 thread_fn对应一个内核线程thread，当handler执行完毕，Linux内核会唤醒对应的内核线程。在内核线程里，会调用 thread_fn函数。 可以提供handler而不提供thread_fn，就退化为一般的request_irq函数。 可以不提供handler只提供thread_fn，完全由内核线程来处理中断。 也可以既提供handler也提供thread_fn，这就是中断上半部、下半部。 在reqeust_irq时可以传入dev_id，为何需要dev_id？作用如下，在共享中断中必须提供dev_id，非共享中断可以不提供。 中断处理函数执行时，可以使用dev_id 卸载中断时要传入dev_id，这样才能在action链表中根据dev_id找到对应项 irq_data结构体 irq_data结构体在 include/linux/irq.h 中定义，主要内容如下： struct irq_data { u32 mask; unsigned int irq; unsigned long hwirq; struct irq_common_data *common; struct irq_chip *chip; struct irq_domain *domain; #ifdef CONFIG_IRQ_DOMAIN_HIERARCHY struct irq_data *parent_data; #endif void *chip_data; }; irq_data 就是个中转站，里面有irq_chip指针 irq_domain指针，都是指向别的结构体。 有意思的是irq、hwirq，irq是软件中断号，hwirq是硬件中断号。比如上面我们举的例子，在GPIO中断B是软件中断号，可以找到irq_desc[B]这个数组项；GPIO里的第x号中断，这就是hwirq。 谁来建立irq、hwirq之间的联系呢？由irq_domain来建立。irq_domain会把本地的hwirq映射为全局的irq，什么意思？比如GPIO控制器里有第1号中断，UART模块里也有第1号中断，这两个“第1号中断”是不一样的，它们属于不同的“域”──irq_domain。 irq_domain结构体 irq_domain结构体在 include/linux/irqdomain.h 中定义，主要内容如下: struct irq_domain { const struct irq_domain_ops *ops; irq_hw_number_t hwirq_max; unsigned int revmap_size; unsigned int linear_revmap[]; ... }; 如何在设备树中指定中断，设备树的中断如何被转换为irq时，irq_domain将会起到极大的作为。 设备树中 interrupt-parent = <&gpio1>; interrupts = <5 IRQ_TYPE_EDGE_RISING>; 它表示要使用 gpio1 里的第5号中断，hwirq就是 5。但是我们在驱动中会使用 request_irq(irq, handler) 这样的函数来注册中断，irq是什么？它是软件中断号，它应该从“gpio1的第5号中断”转换得来。 谁把hwirq转换为irq？由gpio1的相关数据结构，就是gpio1对应的irq_domain结构体。irq_domain结构体中有一个irq_domain_ops结构体，里面有各种操作函数，主要是： xlate 用来解析设备树的中断属性，提取出hwirq、type等信息。 map 把hwirq转换为irq。 irq_chip结构体 irq_chip结构体在 include/linux/irq.h 中定义，主要内容如下 struct irq_chip { // irq_enable: enable the interrupt (defaults to chip->unmask if NULL) void (*irq_enable)(struct irq_data *data); // irq_disable: disable the interrupt void (*irq_disable)(struct irq_data *data); // irq_ack: start of a new interrupt void (*irq_ack)(struct irq_data *data); // irq_mask: mask an interrupt source void (*irq_mask)(struct irq_data *data); // irq_mask_ack: ack and mask an interrupt source void (*irq_mask_ack)(struct irq_data *data); ... }; 我们在request_irq后，并不需要手工去使能中断，原因就是系统调用对应的irq_chip里的函数帮我们使能了中断。 我们提供的中断处理函数中，也不需要执行主芯片相关的清中断操作，也是系统帮我们调用irq_chip中的相关函数。 但是对于外部设备相关的清中断操作，还是需要我们自己做的。外设备千变万化，内核里可没有对应的清除中断操作 在设备树中指定中断_在代码中获得中断 设备树里中断节点的语法 参考文档： 内核 Documentation\devicetree\bindings\interrupt-controller\interrupts.txt 设备树 设备树里的中断控制器 GPIO1有32个中断源，但是它把其中的16个汇聚起来向GIC发出一个中断，把另外16个汇聚起来向GIC发出另一个中断。这就意味着GPIO1会用到GIC的两个中断，会涉及GIC里的2个hwirq。这些层级关系、中断号(hwirq)，都会在设备树中有所体现。 在设备树中，中断控制器节点中必须有一个属性：interrupt-controller，表明它是“中断控制器”。还必须有一个属性：#interrupt-cells，表明引用这个中断控制器的话需要多少个cell。 #interrupt-cells=<1> 只需要一个cell来表明使用“哪一个中断”。 #interrupt-cells=<2> 需要一个cell来表明使用“哪一个中断”；还需要另一个cell来描述中断，一般是表明触发类型： 第2个cell的bits[3:0] 用来表示中断触发类型(trigger type and level flags)： 1 = low-to-high edge triggered，上升沿触发 2 = high-to-low edge triggered，下降沿触发 4 = active high level-sensitive，高电平触发 8 = active low level-sensitive，低电平触发 如果中断控制器有级联关系，下级的中断控制器还需要表明它的“interrupt-parent”是谁，用了interrupt-parent”中的哪一个“interrupts”， 设备树里使用中断 interrupt-parent=<&XXXX> interrupts Interrupts里要用几个cell，由interrupt-parent对应的中断控制器决定。在中断控制器里有 #interrupt-cells 属性，它指明了要用几个cell来描述中断。 interrupts-extended 一个“interrupts-extended”属性就可以既指定“interrupt-parent”，也指定“interrupts”，比如：interrupts-extended = <&intc1 5 1>, <&intc2 1 0>; 设备树里中断节点的示例 在 arch/arm/boot/dts 目录下可以看到2个文件：imx6ull.dtsi、100ask_imx6ull-14x14.dts，把里面有关中断的部分内容抽取出来 ARM GIC 正常可以有分为几类: Shared Peripheral Interrupts (SPIs), Private Peripheral Interrupts (PPIs), Software Generated Interrupts (SGIs), Locality-specific Peripheral Interrupts (LPIs). 上图中，可以看出: gpc 中的 interrupts = <GIC_SPI 89 IRQ_TYPE_LEVEL_HIGH>; 第一个是类别 GIC_SPI， 第二个是中断号，第三个是中断触发类型。 gpio1 中没有 interrupt-parent，所以继承父节点的 interrupt-parent 也就是上级中断是 gpc spidev 中，上级中断是 gpio1, interrupts = <1 1>; 表示中断是 gpio1 中的 1号，上升沿触发。 从设备树反推IMX6ULL的中断体系，如下，比之前的框图多了一个“GPC INTC”： GPC INTC的英文是：General Power Controller, Interrupt Controller。它提供中断屏蔽、中断状态查询功能，它还提供唤醒功能。 在代码中获得中断 设备树中的节点有些能被转换为内核里的platform_device，有些不能 根节点下含有compatile属性的子节点，会转换为platform_device 含有特定compatile属性的节点的子节点，会转换为platform_device. 如果一个节点的compatile属性，它的值是这4者之一："simple-bus","simple-mfd","isa","arm,amba-bus", 那么它的子结点(需含compatile属性)也可以转换为platform_device。 总线I2C、SPI节点下的子节点：不转换为platform_device. 某个总线下到子节点，应该交给对应的总线驱动程序来处理, 它们不应该被转换为platform_device。 对于platform_device 一个节点能被转换为platform_device，如果它的设备树里指定了中断属性，那么可以从platform_device中获得“中断资源”，可以使用下列函数获得IORESOURCE_IRQ资源，即中断号, struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num); resource type 选择 IORESOURCE_IRQ 对于I2C设备、SPI设备 I2C总线驱动在处理设备树里的I2C子节点时，也会处理其中的中断信息。一个I2C设备会被转换为一个i2c_client结构体，中断号会保存在i2c_client的irq成员里. drivers/i2c/i2c-core.c static int i2c_device_probe(struct device *dev) { ... irq = of_irq_get(dev->of_node, 0); ... } 对于SPI设备节点，SPI总线驱动在处理设备树里的SPI子节点时，也会处理其中的中断信息。一个SPI设备会被转换为一个spi_device结构体，中断号会保存在spi_device的irq成员里，drivers/spi/spi.c static int spi_drv_probe(struct device *dev) { ... spi->irq = of_irq_get(dev->of_node, 0); ... } 调用of_irq_get获得中断号 如果你的设备节点既不能转换为platform_device，它也不是I2C设备，不是SPI设备，那么在驱动程序中可以自行调用of_irq_get函数去解析设备树，得到中断号。 对于GPIO 参考：drivers/input/keyboard/gpio_keys.c 可以使用 gpio_to_irq 或 gpiod_to_irq 获得中断号。 // 可以使用下面的函数获得引脚和flag button->gpio = of_get_gpio_flags(pp, 0, &flags); bdata->gpiod = gpio_to_desc(button->gpio); // 再去使用gpiod_to_irq获得中断号 irq = gpiod_to_irq(bdata->gpiod); 编写使用中断的按键驱动程序 对于GPIO按键，我们并不需要去写驱动程序，使用内核自带的驱动程序 drivers/input/keyboard/gpio_keys.c 就可以，然后你需要做的只是修改设备树指定引脚及键值。 编程思路 设备树相关 gpio_keys_100ask { compatible = "100ask,gpio_key"; gpios = <&gpio5 1 GPIO_ACTIVE_HIGH &gpio4 14 GPIO_ACTIVE_HIGH>; pinctrl-names = "default"; pinctrl-0 = <&key1_pinctrl &key2_pinctrl>; }; 代码相关 count = of_gpio_count(node); 从上面的设备树中，gpios 这一项计算引脚数量。根据上级节点的 cells 数量，来计算 count. 从设备树中获取 gpio 相关 gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag); gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio); 申请使用 gpio, 并配置是否低电平有效 err = devm_gpio_request_one(&pdev->dev, gpio_keys_100ask[i].gpio, flags, NULL); 设备树中没有中断号，只能通过 gpio 转为 irq，gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio); 申请中断 err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "100ask_gpio_key", &gpio_keys_100ask[i]); IMX6ULL设备树修改及上机实验 中断相关的其他驱动程序 中断的硬件框架 中断路径上的3个部件 中断源, 中断控制器, CPU. GIC 能够统一管理中断，并且在多核环境下，能够选择发送给某个核或者所有核进行处理。 GIC介绍与编程 介绍 中断可以有多种不同的类型 软件触发中断（SGI，Software Generated Interrupt） 这是由软件通过写入专用仲裁单元的寄存器即软件触发中断寄存器（ICDSGIR）显式生成的。它最常用于CPU核间通信。SGI既可以发给所有的核，也可以发送给系统中选定的一组核心。中断号0-15保留用于SGI的中断号。用于通信的确切中断号由软件决定。 私有外设中断（PPI，Private Peripheral Interrupt） 这是由单个CPU核私有的外设生成的。PPI的中断号为16-31。它们标识CPU核私有的中断源，并且独立于另一个内核上的相同中断源，比如，每个核的计时器。 共享外设中断（SPI，Shared Peripheral Interrupt） 这是由外设生成的，中断控制器可以将其路由到多个核。中断号为32-1020。SPI用于从整个系统可访问的各种外围设备发出中断信号。 中断可以处于多种不同状态： 非活动状态（Inactive）–这意味着该中断未触发。 挂起（Pending）–这意味着中断源已被触发，但正在等待CPU核处理。待处理的中断要通过转发到CPU接口单元，然后再由CPU接口单元转发到内核。 活动（Active）–描述了一个已被内核接收并正在处理的中断。 活动和挂起（Active and pending）–描述了一种情况，其中CPU核正在为中断服务，而GIC又收到来自同一源的中断。 GIC控制器的逻辑结构 上图中，可以看出 SGI 和 PPI 是每个核自己的，SPI 是所有核共享的。 初始化 Distributor和CPU interface在复位时均被禁用。复位后，必须初始化GIC，才能将中断传递给CPU核。 在Distributor中，软件必须配置优先级、目标核、安全性并启用单个中断；随后必须通过其控制寄存器使能。 对于每个CPU interface，软件必须对优先级和抢占设置进行编程。每个CPU接口模块本身必须通过其控制寄存器使能。 在CPU核可以处理中断之前，软件会通过在向量表中设置有效的中断向量并清除CPSR中的中断屏蔽位来让CPU核可以接收中断。 可以通过禁用Distributor单元来禁用系统中的整个中断机制；可以通过禁用单个CPU的CPU接口模块或者在CPSR中设置屏蔽位来禁止向单个CPU核的中断传递。也可以在Distributor中禁用（或启用）单个中断。 为了使某个中断可以触发CPU核，必须将各个中断，Distributor和CPU interface全部使能，并将CPSR中断屏蔽位清零 GIC的寄存器 GIC分为两部分：Distributor和CPU interface，它们的寄存器都有相应的前缀：“GICD”、“GICC”。这些寄存器都是映射为内存接口(memery map)，CPU可以直接读写。 相关寄存器可以参考相关资料 GIC编程 使用cortex A7处理器的芯片，一般都是使用GIC v2的中断控制器。 处理GIC的基地址不一样外，对GIC的操作都是一样的。 在NXP官网可以找到IMX6ULL的SDK包。 下载后可以参考这个文件：core_ca7.h，里面含有GIC的初始化代码。 异常向量表的安装与调用 系统启动流程 引导加载程序（Bootloader）： 负责加载内核镜像（如 zImage 或 vmlinux）到内存的指定位置，传递启动参数（如设备树、命令行参数）传递给内核。并跳转到内核的入口点，通常是 head.S 中的某个标签（如 stext）。 例如：U-Boot、GRUB、LILO 等。 内核解压缩阶段（如果使用压缩内核） 如果内核镜像被压缩（如 zImage），在 head.S 之前会运行解压缩代码（如 arch/arm/boot/compressed/head.S）。 解压缩代码将内核解压到内存的指定位置，然后跳转到 head.S。 内核入口点： 体系结构相关的启动代码，通常是 head.S 或类似的汇编文件。通常位于 arch/<arch>/kernel/head.S 初始化 CPU(设置 CPU 的寄存器、模式、缓存等) 和内存(设置页表、启用 MMU（内存管理单元）)，设置最小化的运行环境。并跳转到 C 代码：调用 start_kernel 函数，进入通用的内核初始化流程。 内核初始化： 调用 start_kernel 函数，进入通用的内核初始化流程（如调度器、内存管理、中断处理等）。 用户空间初始化： 启动 init 进程，进入用户空间。 异常向量表的安装 复制向量表 // arch\arm\kernel\head.S ldr r13, =__mmap_switched @ address to jump to after ... 1: b __enable_mmu === // arch\arm\kernel\head.S __enable_mmu ... b __turn_mmu_on === // arch\arm\kernel\head.S __turn_mmu_on // ret r3 是一个伪指令，用于将程序计数器设置为 r3 的值，从而实现跳转。ret r3 的等价指令是 bx r3，通常用于函数指针调用或间接跳转。 ... mov r3, r13 ret r3 === // arch\arm\kernel\head-common.S __mmap_switched: ... b start_kernel asmlinkage __visible void __init start_kernel(void) // init\main.c setup_arch(&command_line); // arch\arm\kernel\setup.c paging_init(mdesc); // arch\arm\mm\mmu.c devicemaps_init(mdesc); // arch\arm\mm\mmu.c vectors = early_alloc(PAGE_SIZE * 2); // 1.分配新向量表 early_trap_init(vectors); // 2.从代码把向量表复制到新向量表 ... // 3. 映射新向量表到虚拟地址0xffff0000 /* * Create a mapping for the machine vectors at the high-vectors * location (0xffff0000). If we aren't using high-vectors, also * create a mapping at the low-vectors virtual address. */ map.pfn = __phys_to_pfn(virt_to_phys(vectors)); map.virtual = 0xffff0000; map.length = PAGE_SIZE; #ifdef CONFIG_KUSER_HELPERS map.type = MT_HIGH_VECTORS; #else map.type = MT_LOW_VECTORS; #endif create_mapping(&map); 向量表在哪 上面代码中可以看到代码中向量表位于__vectors_start，它在arch/arm/kernel/vmlinux.lds中定义, 可以用 grep 搜索。 __vectors_start = .; .vectors 0xffff0000 : AT(__vectors_start) { *(.vectors) } . = __vectors_start + SIZEOF(.vectors); __vectors_end = .; __stubs_start = .; .stubs ADDR(.vectors) + 0x1000 : AT(__stubs_start) { *(.stubs) } 在代码里搜.vectors，可以找到向量表： // arch\arm\kernel\entry-armv.S .section .vectors, "ax", %progbits .L__vectors_start: W(b) vector_rst W(b) vector_und W(ldr) pc, .L__vectors_start + 0x1000 W(b) vector_pabt W(b) vector_dabt W(b) vector_addrexcptn W(b) vector_irq W(b) vector_fiq 中断向量 发生中断时，CPU跳到向量表去执行b vector_irq。 vector_irq函数使用宏来定义： 处理流程 处理函数 GIC驱动程序对中断的处理流程 参考资料： linux kernel的中断子系统之（七）：GIC代码分析 irq_desc 对于irq_desc，内核有两种分配方法： 一次分配完所有的irq_desc, 把所有层级的硬件中断 hwirq，按照顺序平坦开来映射为 virq。 按需分配用到某个中断才分配它的irq_desc 一级中断控制器处理流程 假设要使用UART模块，它发出的中断连接到GIC的32号中断，分配的irq_desc序号为16 在GIC domain中会记录(32, 16) 那么注册中断时就是：request_irq(16, ...) 发生UART中断时 程序从GIC中读取寄存器知道发生了32号中断，通过GIC irq_domain可以知道virq为16 调用irq_desc[16]中的handleA函数，它的作用是调用action链表中用户注册的函数 多级中断控制器处理流程 假设GPIO模块下有4个引脚，都可以产生中断，都连接到GIC的33号中断 GPIO也可以看作一个中断控制器，对于它的4个中断 对于GPIO模块中0~3这四个hwirq，一般都会一下子分配四个irq_desc 假设这4个irq_desc的序号为100~103，在GPIO domain中记录(0,100) (1,101)(2,102) (3,103) 对于KEY，注册中断时就是：request_irq(102, ...) 按下KEY时： 程序从GIC中读取寄存器知道发生了33号中断，通过GIC irq_domain可以知道virq为16 调用irq_desc[16]中的handleB函数 handleB读取GPIO寄存器，确定是GPIO里2号引脚发生中断 通过GPIO irq_domain可以知道virq为102 调用irq_desc[102]中的handleA函数，它的作用是调用action链表中用户注册的函数 GIC驱动程序分析 参考资料： linux kernel的中断子系统之（七）：GIC代码分析 Linux 4.9.88内核源码 Linux-4.9.88\drivers\irqchip\irq-gic.c Linux-4.9.88/arch/arm/boot/dts/imx6ull.dtsi Linux 5.4内核源码 Linux-5.4\drivers\irqchip\irq-gic.c Linux-5.4/arch/arm/boot/dts/stm32mp151.dtsi GIC中的重要函数和结构体 沿着中断的处理流程，GIC涉及这4个重要部分： CPU从异常向量表中调用handle_arch_irq，这个函数指针是有GIC驱动设置的 GIC才知道怎么判断发生的是哪个GIC中断 从GIC获得hwirq后，要转换为virq：需要有GIC Domain 调用irq_desc[virq].handle_irq函数：这也应该由GIC驱动提供 处理中断时，要屏蔽中断、清除中断等：这些函数保存在irq_chip里，由GIC驱动提供 从硬件上看，GIC的功能是什么？ 可以使能、屏蔽中断 发生中断时，可以从GIC里判断是哪个中断 在内核里，使用gic_chip_data结构体表示GIC，gic_chip_data里有什么？ irq_chip：中断使能、屏蔽、清除，放在irq_chip中的各个函数里实现 // drivers\irqchip\irq-gic.c static struct irq_chip gic_chip = { .irq_mask = gic_mask_irq, .irq_unmask = gic_unmask_irq, .irq_eoi = gic_eoi_irq, .irq_set_type = gic_set_type, .irq_get_irqchip_state = gic_irq_get_irqchip_state, .irq_set_irqchip_state = gic_irq_set_irqchip_state, .flags = IRQCHIP_SET_TYPE_MASKED | IRQCHIP_SKIP_SET_WAKE | IRQCHIP_MASK_ON_SUSPEND, }; irq_domain 申请中断时 在设备树里指定hwirq、flag，可以使用irq_domain的函数来解析设备树 根据hwirq可以分配virq，把(hwirq, virq)存入irq_domain中 发生中断时，从GIC读出hwirq，可以通过irq_domain找到virq，从而找到处理函数 所以，GIC用gic_chip_data来表示，gic_chip_data中重要的成员是：irq_chip、irq_domain。 GIC初始化过程 start_kernel (init\main.c) init_IRQ (arch\arm\kernel\irq.c) irqchip_init (drivers\irqchip\irqchip.c) of_irq_init (drivers\of\irq.c) desc->irq_init_cb = match->data; ... ret = desc->irq_init_cb(desc->dev, desc->interrupt_parent); 设备树 从 dtb 反编译为 dts 在 dts 中寻找 interrupt-controlls，找到最顶层的那个 gic 在 gic 节点中，找到 compatible="arm,cortex-a7-gic", 通过这个 compatible 搜索驱动，找到对应的 driver 源码。 流程 grep -nr "arm,cortex-a7-gic" --include="*.c" drivers/ 找到源码 drivers\irqchip\irq-gic.c IRQCHIP_DECLARE(cortex_a7_gic, "arm,cortex-a7-gic", gic_of_init); 宏定义 // include\linux\irqchip.h #define IRQCHIP_DECLARE(name, compat, fn) OF_DECLARE_2(irqchip, name, compat, fn) // include\linux\of.h #define OF_DECLARE_2(table, name, compat, fn) \ _OF_DECLARE(table, name, compat, fn, of_init_fn_2) // ... #if defined(CONFIG_OF) && !defined(MODULE) #define _OF_DECLARE(table, name, compat, fn, fn_type) \ static const struct of_device_id __of_table_##name \ __used __section(__##table##_of_table) \ = { .compatible = compat, \ .data = (fn == (fn_type)NULL) ? fn : fn } #else #define _OF_DECLARE(table, name, compat, fn, fn_type) \ static const struct of_device_id __of_table_##name \ __attribute__((unused)) \ = { .compatible = compat, \ .data = (fn == (fn_type)NULL) ? fn : fn } #endif IRQCHIP_DECLARE(cortex_a7_gic, "arm,cortex-a7-gic", gic_of_init); 展开后得到如下: 对应的函数是 gic_of_init, 并且在 __irqchip_of_table 这个 section 里面。这对应到 drivers\irqchip\irqchip.c 中的 void __init irqchip_init(void) 中的 of_irq_init(__irqchip_of_table); static const struct of_device_id __of_table_cortex_a7_gic \ __used __section(__irqchip_of_table) \ = { .compatible = "arm,cortex-a7-gic", \ .data = gic_of_init } // drivers\irqchip\irqchip.c void __init irqchip_init(void) { of_irq_init(__irqchip_of_table); acpi_probe_device_table(irqchip); } 整体的流程: gic_of_init int __init gic_of_init(struct device_node *node, struct device_node *parent) ret = gic_of_setup(gic, node); 根据设备树进行设置，包含寄存器地址等。 ret = gic_of_setup(gic, node); gic->raw_dist_base = of_iomap(node, 0); gic->raw_cpu_base = of_iomap(node, 1); ret = __gic_init_bases(gic, -1, &node->fwnode); ret = __gic_init_bases(gic, -1, &node->fwnode); set_handle_irq(gic_handle_irq); // 设置 gic_handle_irq， 和前面的其他地方呼应起来。 gic_init_chip(gic, NULL, name, true); ret = gic_init_bases(gic, irq_start, handle); gic->domain = irq_domain_create_linear(handle, gic_irqs, &gic_irq_domain_hierarchy_ops, gic); static inline struct irq_domain *irq_domain_create_linear(struct fwnode_handle *fwnode, unsigned int size, const struct irq_domain_ops *ops, void *host_data) 用到的 irq_domain_ops, 其中 gic_irq_domain_translate 用于解析设备树中的中断相关的 hwirq 和 flag. gic_irq_domain_alloc 用于申请 irq_desc 数组中新的空闲 virq, 并设置相关信息，然后保存 hwirq 和 virq 的关联到 domain 中。 static const struct irq_domain_ops gic_irq_domain_hierarchy_ops = { .translate = gic_irq_domain_translate, .alloc = gic_irq_domain_alloc, .free = irq_domain_free_irqs_top, }; 申请GIC中断 设备树 内核对设备树的处理 为设备树节点分配设备 of_device_alloc (drivers/of/platform.c) dev = platform_device_alloc("", -1); // 分配 platform_device num_irq = of_irq_count(np); // 计算中断数 // drivers/of/irq.c, 根据设备节点中的中断信息, 构造中断资源 of_irq_to_resource_table(np, res, num_irq) of_irq_to_resource // drivers\of\irq.c int irq = irq_of_parse_and_map(dev, index); // 获得virq, 中断号 解析设备树映射中断: irq_of_parse_and_map // drivers/of/irq.c, 解析设备树中的中断信息, 保存在of_phandle_args结构体中 of_irq_parse_one(dev, index, &oirq) // kernel/irq/irqdomain.c, 创建中断映射 irq_create_of_mapping(&oirq); irq_create_fwspec_mapping(&fwspec); // 调用irq_domain->ops的translate或xlate,把设备节点里的中断信息解析为hwirq, type irq_domain_translate(domain, fwspec, &hwirq, &type) // 看看这个hwirq是否已经映射, 如果virq非0就直接返回 virq = irq_find_mapping(domain, hwirq); // 否则创建映射 if (irq_domain_is_hierarchy(domain)) { // 返回未占用的virq // 并用irq_domain->ops->alloc函数设置irq_desc virq = irq_domain_alloc_irqs(domain, 1, NUMA_NO_NODE, fwspec); if (virq <= 0) return 0; } else { /* Create mapping */ // 返回未占用的virq // 并通过irq_domain_associate调用irq_domain->ops->map设置irq_desc virq = irq_create_mapping(domain, hwirq); if (!virq) return virq; } 分析 drivers\irqchip\irq-gic.c 中 static int gic_irq_domain_translate(struct irq_domain *d, struct irq_fwspec *fwspec, unsigned long *hwirq, unsigned int *type) 因为设备树中的 GIC_SPI 是 0， 所以 interrupts = <GIC_SPI 89 IRQ_TYPE_LEVEL_HIGH>; 中的 89 在内核中 hwirq = 89 + 16 + 16 = 121 /* Get the interrupt number and add 16 to skip over SGIs */ *hwirq = fwspec->param[1] + 16; /* * For SPIs, we need to add 16 more to get the GIC irq * ID number */ if (!fwspec->param[0]) *hwirq += 16; 在开发板上，命令 cat /proc/interrupts 可以得到详细中断情况 当然也可以在 /sys/kernel/irq/ 中，通过 grep -nr "121" 来搜索 hwirq 121 号中断。在相应的 virq 子文件夹中，cat 相关的 hwirq、 chip_name 来获取更加详细的数据。 chip_name 中断名 hwirq 设备树中中断号加过 32 或者 16 之后的数值，用于内核 per_cpu_count 每个核上面的中断次数 actions 自己定义的函数 type 中断引脚类型，水平触发还是边沿触发。 两类中断控制器处理流程_链式和层级
GPIO子系统视频介绍 参考资料： Linux 5.x内核文档 Linux-5.4\Documentation\driver-api Linux-5.4\Documentation\devicetree\bindings\gpio\gpio.txt Linux 4.x内核文档 Linux-4.9.88\Documentation\gpio Linux-4.9.88\Documentation\devicetree\bindings\gpio\gpio.txt 通用属性 Active-High and Active-Low gpiod_set_value(gpio, 1); // 输出逻辑1 // 在Active-High的情况下它会输出高电平 // 在Active-Low的情况下它会输出低电平 Open Drain and Open Source 多个GPIO驱动同时驱动一个电路时，就需要设置Open Drain或Open Source。 Open Drain：引脚被设置为低电平时才会驱动电路，典型场景是I2C接口。 Open Source：引脚被设置为高电平时才会驱动电路 使用GPIO子系统要掌握的重要概念 设备树 在设备树 dtsi 中，“GPIO组”就是一个GPIO Controller，这通常都由芯片厂家设置好。我们要做的是找到它名字，比如“gpio1”，然后在 dts 中指定要用它里面的哪个引脚，比如<&gpio1 0>。 下图是一些芯片的GPIO控制器节点，它们一般都是厂家定义好，在xxx.dtsi文件中： 我们暂时只需要关心里面的这2个属性： “gpio-controller”表示这个节点是一个GPIO Controller，它下面有很多引脚。 “#gpio-cells = ”表示这个控制器下每一个引脚要用2个32位的数(cell)来描述。这个 2 指的是除了gpioN 以外还要 2个值来描述。 普遍的用法是，用第1个cell来表示哪一个引脚，用第2个cell来表示有效电平。例如在使用引脚的时候，如 gpios = <&gpio5 3 GPIO_ACTIVE_LOW> 在自己的设备节点中使用属性[<name>-]gpios，name 用来做标识作用，示例如下： 代码 GPIO子系统有两套接口：基于描述符的(descriptor-based)、老的(legacy)。前者的函数都有前缀“gpiod_”，它使用gpiodesc结构体来表示一个引脚；后者的函数都有前缀“gpio”，它使用一个整数来表示一个引脚。 驱动程序中要包含头文件， #include <linux/gpio/consumer.h> // descriptor-based 或 #include <linux/gpio.h> // legacy 常用的函数： 获得GPIO descriptor-based legacy 说明 gpiod_get gpio_request gpiod_get_index gpiod_get_array gpio_request_array devm_gpiod_get devm_gpiod_get_index devm_gpiod_get_array 设置方向 descriptor-based legacy 说明 gpiod_direction_input gpio_direction_input gpiod_direction_output gpio_direction_output 读值、写值 descriptor-based legacy 说明 gpiod_get_value gpio_get_value gpiod_set_value gpio_set_value 释放GPIO descriptor-based legacy 说明 gpio_free gpio_free gpiod_put gpio_free_array gpiod_put_array devm_gpiod_put devm_gpiod_put_array 有前缀devm_的含义是“设备资源管理”(Managed Device Resource)，这是一种自动释放资源的机制。比如在Linux开发过程中，先申请了GPIO，再申请内存；如果内存申请失败，那么在返回之前就需要先释放GPIO资源。如果使用devm的相关函数，在内存申请失败时可以直接返回：设备的销毁函数会自动地释放已经申请了的GPIO资源。建议使用devm_版本的相关函数。 代码从设备树中获取数值 foo_device { compatible = "acme,foo"; ... led-gpios = <&gpio 15 GPIO_ACTIVE_HIGH>, /* red */ <&gpio 16 GPIO_ACTIVE_HIGH>, /* green */ <&gpio 17 GPIO_ACTIVE_HIGH>; /* blue */ power-gpios = <&gpio 1 GPIO_ACTIVE_LOW>; }; struct gpio_desc *red, *green, *blue, *power; red = gpiod_get_index(dev, "led", 0, GPIOD_OUT_HIGH); green = gpiod_get_index(dev, "led", 1, GPIOD_OUT_HIGH); blue = gpiod_get_index(dev, "led", 2, GPIOD_OUT_HIGH); power = gpiod_get(dev, "power", GPIOD_OUT_HIGH); 要注意的是，gpiod_set_value设置的值是“逻辑值”，不一定等于物理值。 旧的“gpio_”函数没办法根据设备树信息获得引脚，它需要先知道引脚号。在GPIO子系统中，每注册一个GPIO Controller时会确定它的“base number”，那么这个控制器里的第n号引脚的号码就是：base number + n。但是如果硬件有变化、设备树有变化，这个base number并不能保证是固定的，应该查看sysfs来确定base number。 sysfs中的访问方法_IMX6ULL 在sysfs中访问GPIO，实际上用的就是引脚号，老的方法。 计算引脚号 先确定某个GPIO Controller的基准引脚号(base number)，再计算出某个引脚的号码。 先在开发板的/sys/class/gpio目录下，找到各个gpiochipXXX目录 然后进入某个gpiochip目录，查看文件label的内容 根据label的内容对比设备树. label内容来自设备树，比如它的寄存器基地址。用来跟设备树(dtsi文件)比较，就可以知道这对应哪一个GPIO Controller。 基于sysfs操作引脚 echo 110 > /sys/class/gpio/export echo in > /sys/class/gpio/gpio110/direction cat /sys/class/gpio/gpio110/value echo 110 > /sys/class/gpio/unexport 注意：如果驱动程序已经使用了该引脚，那么将会export失败，会提示 busy. 对于输出引脚，假设引脚号为N，可以用下面的方法设置它的值为1: echo N > /sys/class/gpio/export echo out > /sys/class/gpio/gpioN/direction echo 1 > /sys/class/gpio/gpioN/value echo N > /sys/class/gpio/unexport 基于GPIO子系统的LED驱动程序 GPIO的地位跟其他模块，比如I2C、UART的地方是一样的，要使用某个引脚，需要先把引脚配置为GPIO功能，这要使用Pinctrl子系统，只需要在设备树里指定就可以。在驱动代码上不需要我们做任何事情。 编程示例 先按照正常的驱动框架去写，再进行细化。 probe gpiod_get 的第二个参数对应到设备树中的名称需要去掉后面的 -gpios. 比如如果参数是 "led" 那么对应的设备树中是 led-gpios = <&gpio0 18 GPIO_ACTIVE_HIGH>. 86 /* 4.1 设备树中定义有: led-gpios=<...>; */ 87 led_gpio = gpiod_get(&pdev->dev, "led", 0); 88 if (IS_ERR(led_gpio)) { 89 dev_err(&pdev->dev, "Failed to get GPIO for led\n"); 90 return PTR_ERR(led_gpio); 91 } 93 /* 4.2 注册file_operations */ 94 major = register_chrdev(0, "100ask_led", &led_drv); /* /dev/led */ 95 96 led_class = class_create(THIS_MODULE, "100ask_led_class"); 97 if (IS_ERR(led_class)) { 98 printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); 99 unregister_chrdev(major, "led"); 100 gpiod_put(led_gpio); 101 return PTR_ERR(led_class); 102 } 103 104 device_create(led_class, NULL, MKDEV(major, 0), NULL, "100ask_led%d", 0); /*dev/100ask_led0 */ 105 open函数中调用GPIO函数设置引脚方向 51 static int led_drv_open (struct inode *node, struct file *file) 52 { 53 //int minor = iminor(node); 54 55 printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); 56 /* 根据次设备号初始化LED */ 57 gpiod_direction_output(led_gpio, 0); 58 59 return 0; 60 } write函数中调用GPIO函数设置引脚值 34 /* write(fd, &val, 1); */ 35 static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset) 36 { 37 int err; 38 char status; 39 //struct inode *inode = file_inode(file); 40 //int minor = iminor(inode); 41 42 printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); 43 err = copy_from_user(&status, buf, 1); 44 45 /* 根据次设备号和status控制LED */ 46 gpiod_set_value(led_gpio, status); 47 48 return 1; 49 } 释放GPIO gpiod_put(led_gpio); 在100ASK_IMX6ULL上机实验 设备树中修改: &iomuxc_snvs { …… imx6ul-evk { myled_for_gpio_subsys: myled_for_gpio_subsys{ fsl,pins = < MX6ULL_PAD_SNVS_TAMPER3__GPIO5_IO03 0x000110A0 >; }; …… } myled { compatible = "100ask,leddrv"; pinctrl-names = "default"; pinctrl-0 = <&myled_for_gpio_subsys>; led-gpios = <&gpio5 3 GPIO_ACTIVE_LOW>; }; GPIO子系统层次与数据结构 参考资料： Linux 5.x内核文档 Linux-5.4\Documentation\driver-api Linux-5.4\Documentation\devicetree\bindings\gpio\gpio.txt Linux-5.4\drivers\gpio\gpio-74x164.c Linux 4.x内核文档 Linux-4.9.88\Documentation\gpio Linux-4.9.88\Documentation\devicetree\bindings\gpio\gpio.txt Linux-4.9.88\drivers\gpio\gpio-74x164.c 层次 GPIOLIB向下提供的接口 在 drivers\gpio\gpiolib.c 中的 int gpiochip_add_data(struct gpio_chip *chip, void *data) 重要的3个核心数据结构 以Linux面向对象编程的思想，一个GPIO Controller必定会使用一个结构体来表示，这个结构体必定含有这些信息： GPIO引脚信息. 有多少个引脚？有哪些引脚？ 控制引脚的函数. 设置引脚方向、读取/设置数值 中断相关的函数. 把引脚转换为中断 三个核心数据结构的关系 在 Linux 内核的 GPIO 子系统中，gpio_device、gpio_chip 和 gpio_desc 是三个核心数据结构，它们共同管理 GPIO 设备的硬件和软件资源。以下是它们的关系和作用： gpio_device 作用 表示一个 GPIO 设备，是 GPIO 子系统的最高层次结构。 每个 GPIO 设备对应一个 gpio_device，通常与硬件中的 GPIO 控制器（如 SoC 中的 GPIO 模块）相对应。 关键字段 字段 说明 chip 指向 gpio_chip 结构体的指针，表示 GPIO 控制器。 base GPIO 的起始编号（全局编号）。 ngpio GPIO 的数量。 label GPIO 设备的名称（如 "gpiochip0"）。 使用场景 内核内部使用，用于管理 GPIO 设备和全局 GPIO 编号。 用户空间通过 /dev/gpiochipX 访问 GPIO 设备。 gpio_chip 作用 表示一个 GPIO 控制器，是 GPIO 设备的硬件抽象。 每个 GPIO 控制器对应一个 gpio_chip，提供对 GPIO 引脚的操作函数（如设置方向、读取值、写入值等）。 关键字段 字段 说明 label GPIO 控制器的名称（如 "gpiochip0"）。 ngpio GPIO 的数量。 base GPIO 的起始编号（全局编号）。 direction_input 函数指针，用于将 GPIO 设置为输入模式。 direction_output 函数指针，用于将 GPIO 设置为输出模式。 get 函数指针，用于读取 GPIO 的值。 set 函数指针，用于设置 GPIO 的值。 使用场景 驱动开发者实现 gpio_chip，提供 GPIO 控制器的硬件操作函数。 内核通过 gpio_chip 操作 GPIO 引脚。 gpio_desc 作用 表示一个 GPIO 引脚，是 GPIO 引脚的软件抽象。 每个 GPIO 引脚对应一个 gpio_desc，用于管理引脚的状态和操作。 关键字段 字段 说明 chip 指向 gpio_chip 结构体的指针，表示 GPIO 控制器。 flags GPIO 的标志（如方向、状态等）。 label GPIO 引脚的名称（如 "led"）。 使用场景 驱动开发者通过 gpio_desc 操作 GPIO 引脚（如设置方向、读取值、写入值等）。 用户空间通过 libgpiod 库访问 GPIO 引脚。 三者的关系 以下是 gpio_device、gpio_chip 和 gpio_desc 的关系： gpio_device | |-- gpio_chip | | | |-- direction_input | |-- direction_output | |-- get | |-- set | |-- gpio_desc | |-- chip |-- flags |-- label gpio_device： 表示一个 GPIO 设备，管理多个 gpio_chip 和 gpio_desc。 提供全局 GPIO 编号和设备管理功能。 gpio_chip： 表示一个 GPIO 控制器，提供对 GPIO 引脚的操作函数。 每个 gpio_chip 对应一个硬件 GPIO 模块。 gpio_desc： 表示一个 GPIO 引脚，管理引脚的状态和操作。 每个 gpio_desc 对应一个具体的 GPIO 引脚。 总结 gpio_device：表示一个 GPIO 设备，管理全局 GPIO 编号和设备资源。 gpio_chip：表示一个 GPIO 控制器，提供对 GPIO 引脚的操作函数。 gpio_desc：表示一个 GPIO 引脚，管理引脚的状态和操作。 三者共同构成了 Linux GPIO 子系统的核心，用于管理 GPIO 设备和引脚。 gpio_device 每个GPIO Controller用一个gpio_device来表示： 里面每一个gpio引脚用一个gpio_desc来表示 gpio引脚的函数(引脚控制、中断相关)，都放在gpio_chip里 gpio_chip 我们并不需要自己创建gpio_device，编写驱动时要创建的是gpio_chip，里面提供了： 控制引脚的函数 中断相关的函数 引脚信息：支持多少个引脚？各个引脚的名字？ gpio_desc 我们去使用GPIO子系统时，首先是获得某个引脚对应的gpio_desc。 gpio_device表示一个GPIO Controller，里面支持多个GPIO。 在gpio_device中有一个gpio_desc数组，每一引脚有一项gpio_desc。 编写GPIO Controller驱动程序 分配、设置、注册gpioc_chip结构体，示例：drivers\gpio\gpio-74x164.c IMX6ULL的GPIO驱动源码分析 参考资料： Linux 4.x内核文档 Linux-4.9.88\Documentation\gpio Linux-4.9.88\Documentation\devicetree\bindings\gpio\gpio.txt Linux-4.9.88\drivers\gpio\gpio-mxc.c Linux-4.9.88\arch\arm\boot\dts\imx6ull.dtsi 设备树 已有的设备树文件分为 dts, dtsi 等，直接看比较麻烦。 简单点，可以把 dtb 反汇编为单个的 dts 文件，然后去里面查找相对容易点。 通过 gpio-controller 找到 gpio 的节点，然后通过 grep 搜索这个节点的名称，找到对应的 dts 源文件。 通过 compatible 在源码中找到对应的驱动。 GPIO控制器的设备树中，有两项是必须的： gpio-controller : 表明这是一个GPIO控制器 gpio-cells : 指定使用多少个cell(就是整数)来描述一个引脚 源码 Linux-4.9.88\drivers\gpio\gpio-mxc.c static int mxc_gpio_probe(struct platform_device *pdev) 分配 struct mxc_gpio_port *port; port = devm_kzalloc(&pdev->dev, sizeof(*port), GFP_KERNEL); 设置gpio_chip err = bgpio_init(&port->gc, &pdev->dev, 4, port->base + GPIO_PSR, port->base + GPIO_DR, NULL, port->base + GPIO_GDIR, NULL, BGPIOF_READ_OUTPUT_REG_SET); port->gc.request = mxc_gpio_request; port->gc.free = mxc_gpio_free; port->gc.parent = &pdev->dev; port->gc.to_irq = mxc_gpio_to_irq; port->gc.base = (pdev->id < 0) ? of_alias_get_id(np, "gpio") * 32 : pdev->id * 32; of_alias_get_id 这个是从设备树的 aliases 里面获取 gpio 后面的值。比如 gpio0 = &gpio1; of_alias_get_id 返回的就是 0. 注册gpio_chip err = devm_gpiochip_add_data(&pdev->dev, &port->gc, port); 编写一个虚拟GPIO控制器的驱动程序 参考资料： Linux 5.x内核文档 Linux-5.4\Documentation\driver-api Linux-5.4\Documentation\devicetree\bindings\gpio\gpio.txt Linux-5.4\drivers\gpio\gpio-74x164.c Linux 4.x内核文档 Linux-4.9.88\Documentation\gpio Linux-4.9.88\Documentation\devicetree\bindings\gpio\gpio.txt Linux-4.9.88\drivers\gpio\gpio-74x164.c 设备树 gpio_virt: virtual_gpiocontroller { compatible = "100ask,virtual_gpio"; gpio-controller; #gpio-cells = <2>; ngpios = <4>; }; ngpios 表明支持 4个引脚。 源码 还是分配，设置，注册这一套。 在设置的时候，需要专门设置一下 ngpio, 数值从设备树中获取。 设置的时候，如果 gc.base = -1 是让系统自动设置 base 值。 调试与使用虚拟的GPIO控制器 设备树 / { gpio_virt: virtual_gpiocontroller { compatible = "100ask,virtual_gpio"; gpio-controller; #gpio-cells = <2>; ngpios = <4>; }; myled { compatible = "100ask,leddrv"; led-gpios = <&gpio_virt 2 GPIO_ACTIVE_LOW>; }; }; GPIO子系统与Pinctrl子系统的交互 使用GPIO前应该设置Pinctrl 要使用pinA来控制LED，首先要通过Pinctrl子系统把它设置为GPIO功能，然后才能设置它为输出引脚、设置它的输出值。所以在设备树文件里，应该添加Pinctrl的内容： virtual_pincontroller { compatible = "100ask,virtual_pinctrl"; myled_pin: myled_pin { functions = "gpio"; groups = "pin0"; configs = <0x11223344>; }; }; gpio_virt: virtual_gpiocontroller { compatible = "100ask,virtual_gpio"; gpio-controller; #gpio-cells = <2>; ngpios = <4>; }; myled { compatible = "100ask,leddrv"; led-gpios = <&gpio_virt 0 GPIO_ACTIVE_LOW>; pinctrl-names = "default"; pinctrl-0 = <&myled_pin>; }; 但是很多芯片，并不要求在设备树中把把引脚复用为GPIO功能。一个引脚可能被用作GPIO，也可能被用作I2C，GPIO和I2C这些功能是相同地位的。要用作GPIO，需要先通过Pinctrl把引脚复用为GPIO功能。 但是Pinctrl和GPIO关系密切，当你使用gpiod_get获得GPIO引脚时，它就偷偷地通过Pinctrl把引脚复用为GPIO功能了。 我觉得原因是，一般情况下，引脚默认的配置就是 gpio，所以，在作为 gpio 的时候，有些芯片，会自动的用 pinctrl 来配置。 GPIO和Pinctrl的映射关系 示例 左边的Pinctrl支持8个引脚，在Pinctrl的内部编号为0~7 图中有2个GPIO控制器 GPIO0内部引脚编号为0~3，假设在GPIO子系统中全局编号为100~103 GPIO1内部引脚编号为0~3，假设在GPIO子系统中全局编号为104~107 假设我们要使用pin1_1，应该这样做： 根据GPIO1的内部编号1，可以换算为Pinctrl子系统中的编号5 使用Pinctrl的函数，把第5个引脚配置为GPIO功能 这种对应关系可以通过设备树中的 gpio-ranges 来表明 gpio 和 pinctrl 直接的关联。 // 当前GPIO控制器的0号引脚, 对应pinctrlA中的128号引脚, 数量为12 gpio-ranges = <&pinctrlA 0 128 12>; 数据结构 GPIO调用Pinctrl的过程 可以从 drivers\gpio\gpiolib.c 中的 gpiod_get 开始分析. GPIO子系统中的request函数，用来申请某个GPIO引脚，它会导致Pinctrl子系统中的这2个函数之一被调用：pmxops->gpio_request_enable或pmxops->request 调用关系如下： gpiod_get gpiod_get_index desc = of_find_gpio(dev, con_id, idx, &lookupflags); ret = gpiod_request(desc, con_id ? con_id : devname); ret = gpiod_request_commit(desc, label); if (chip->request) { ret = chip->request(chip, offset); } 我们编写GPIO驱动程序时，所设置chip->request函数，一般直接调用gpiochip_generic_request，它导致Pinctrl把引脚复用为GPIO功能。 gpiochip_generic_request(struct gpio_chip *chip, unsigned offset) pinctrl_request_gpio(chip->gpiodev->base + offset) ret = pinctrl_get_device_gpio_range(gpio, &pctldev, &range); // gpio是引脚的全局编号 /* Convert to the pin controllers number space */ pin = gpio_to_pin(range, gpio); ret = pinmux_request_gpio(pctldev, range, pin, gpio); ret = pin_request(pctldev, pin, owner, range); Pinctrl子系统中的pin_request函数就会把引脚配置为GPIO功能： static int pin_request(struct pinctrl_dev *pctldev, int pin, const char *owner, struct pinctrl_gpio_range *gpio_range) { const struct pinmux_ops *ops = pctldev->desc->pmxops; /* * If there is no kind of request function for the pin we just assume * we got it by default and proceed. */ if (gpio_range && ops->gpio_request_enable) /* This requests and enables a single GPIO pin */ status = ops->gpio_request_enable(pctldev, gpio_range, pin); else if (ops->request) status = ops->request(pctldev, pin); else status = 0; } 我们要做什么 设备树表明联系 // 当前GPIO控制器的0号引脚, 对应pinctrlA中的128号引脚, 数量为12 gpio-ranges = <&pinctrlA 0 128 12>; 源码解析联系 不需要我们自己写代码, 注册gpio_chip时会自动调用. int gpiochip_add_data(struct gpio_chip *chip, void *data) status = of_gpiochip_add(chip); status = of_gpiochip_add_pin_range(chip); of_gpiochip_add_pin_range for (;; index++) { ret = of_parse_phandle_with_fixed_args(np, "gpio-ranges", 3, index, &pinspec); pctldev = of_pinctrl_get(pinspec.np); // 根据gpio-ranges的第1个参数找到pctldev // 增加映射关系 /* npins != 0: linear range */ ret = gpiochip_add_pin_range(chip, pinctrl_dev_get_devname(pctldev), pinspec.args[0], pinspec.args[1], pinspec.args[2]); 实现 在自己的驱动程序中，需要实现以下函数: 在GPIO驱动程序中，提供gpio_chip->request 在Pinctrl驱动程序中，提供pmxops->gpio_request_enable或pmxops->request 编程_GPIO使用Pinctrl 设备树 表明GPIO和Pinctrl间的联系, 需要增加类似下面的写法: // 当前GPIO控制器的0号引脚, 对应pinctrlA中的128号引脚, 数量为12 gpio-ranges = <&pinctrlA 0 128 12>; 未增加联系 virtual_pincontroller { compatible = "100ask,virtual_pinctrl"; myled_pin: myled_pin { functions = "gpio"; groups = "pin0"; configs = <0x11223344>; }; }; gpio_virt: virtual_gpiocontroller { compatible = "100ask,virtual_gpio"; gpio-controller; #gpio-cells = <2>; ngpios = <4>; }; myled { compatible = "100ask,leddrv"; led-gpios = <&gpio_virt 0 GPIO_ACTIVE_LOW>; pinctrl-names = "default"; pinctrl-0 = <&myled_pin>; }; 增加联系 pinctrl_virt: virtual_pincontroller { compatible = "100ask,virtual_pinctrl"; }; gpio_virt: virtual_gpiocontroller { compatible = "100ask,virtual_gpio"; gpio-controller; #gpio-cells = <2>; ngpios = <4>; gpio-ranges = <&pinctrl_virt 0 0 4>; }; myled { compatible = "100ask,leddrv"; led-gpios = <&gpio_virt 0 GPIO_ACTIVE_LOW>; }; 源码 gpio gpio_chip 驱动中提供request函数并配置： chip->request = gpiochip_generic_request; pinctrl pinctrl 驱动中提供 gpio_request_enable 函数并配置： static const struct pinmux_ops virtual_pmx_ops = { .get_functions_count = virtual_pmx_get_funcs_count, .get_function_name = virtual_pmx_get_func_name, .get_function_groups = virtual_pmx_get_groups, .set_mux = virtual_pmx_set, .gpio_request_enable = virtual_pmx_gpio_request_enable, }; IMX6ULL 和 STM32MP157 IMX6ULL IMX6ULL使用GPIO时必须设置Pinctrl，如果不设置，只有那些默认就是GPIO功能的引脚可以正常使用。原因是： GPIO控制器的设备树中，没有gpio-ranges Pinctrl驱动中并没有提供pmxops->gpio_request_enable或pmxops->request gpio_chip结构体中direction_input、direction_output，并没有配置引脚为GPIO功能 STM32MP157 在STM32MP157的内核中，Pinctrl驱动中并没有提供pmxops->gpio_request_enable或pmxops->request，为什么也可一直接使用GPIO功能？ 它的gpio_chip结构体中，有direction_input、direction_output，这2个函数的调用关系如下: direction_output/direction_input pinctrl_gpio_direction ret = pinmux_gpio_direction(pctldev, range, pin, input); ret = ops->gpio_set_direction(pctldev, range, pin, input); stm32_pmx_gpio_set_direction stm32_pmx_set_mode // 它会设置引脚为GPIO功能 GPIO子系统的sysfs接口 参考资料： Linux 5.x内核文档 Linux-5.4\Documentation\driver-api Linux-5.4\Documentation\devicetree\bindings\gpio\gpio.txt Linux-5.4\drivers\gpio\gpiolib-sysfs.c Linux 4.x内核文档 Linux-4.9.88\Documentation\gpio Linux-4.9.88\Documentation\devicetree\bindings\gpio\gpio.txt Linux-4.9.88\drivers\gpio\gpiolib-sysfs.c 驱动 驱动程序为drivers\gpio\gpiolib-sysfs.c. 常用的SYSFS文件 查看GPIO控制器 /sys/bus/gpio/devices目录下，列出了所有的GPIO控制器 GPIO控制器的详细信息 /sys/class/gpio/gpiochipXXX下，有这些信息: /sys/class/gpio/gpiochip508]# ls -1 base // 这个GPIO控制器的GPIO编号 device label // 名字 ngpio // 引脚个数 power subsystem uevent 查看GPIO使用情况 cat /sys/kernel/debug/gpio 通过SYSFS使用GPIO 确定GPIO编号 查看每个/sys/class/gpio/gpiochipXXX目录下的label，确定是你要用的GPIO控制器，也称为GPIO Bank。 根据它名字gpiochipXXX，就可以知道基值是XXX。 基值加上引脚offset，就是这个引脚的编号。 导出/设置方向/读写值 echo 509 > /sys/class/gpio/export echo out > /sys/class/gpio/gpio509/direction echo 1 > /sys/class/gpio/gpio509/value echo 509 > /sys/class/gpio/unexport echo 509 > /sys/class/gpio/export echo in > /sys/class/gpio/gpio509/direction cat /sys/class/gpio/gpio509/value echo 509 > /sys/class/gpio/unexport 总结 正常情况下: 需要先用 pinctrl 配置为 gpio, 然后再用 gpio 详细设置。设备树中，也需要都写出来。 特殊情况: 有些芯片厂商开了后门，可以只配置 gpio，就可以使用。 设备树中，可以简略配置。 我们正常只需要在设备树中配置即可。pinctrl 和 gpio 这个是 bsp 工程师来实现。
介绍 参考资料： Linux 5.x内核文档 Documentation\devicetree\bindings\pinctrl\pinctrl-bindings.txt Linux 4.x内核文档 Documentation\pinctrl.txt Documentation\devicetree\bindings\pinctrl\pinctrl-bindings.txt 真正去写驱动的是 bsp 工程师； 驱动工程师，只要了解，然后会用即可。 Pinctrl子系统重要概念 主要参考文档是：内核 Documentation\devicetree\bindings\pinctrl\pinctrl-bindings.txt 这会涉及2个对象：pin controller、client device。 前者提供服务：可以用它来复用引脚、配置引脚。 在芯片手册里你找不到pin controller，它是一个软件上的概念，你可以认为它对应IOMUX──用来复用引脚，还可以配置引脚(比如上下拉电阻等)。 后者使用服务：声明自己要使用哪些引脚的哪些功能，怎么配置它们。 Pinctrl系统的客户，那就是使用Pinctrl系统的设备，使用引脚的设备。它在设备树里会被定义为一个节点，在节点里声明要用哪些引脚。 示例 上图中，左边是pin controller节点，右边是client device节点： pin state： 上图中，pinctrl-names里定义了2种状态：default、sleep。 第0种状态用到的引脚在pinctrl-0中定义，它是state_0_node_a，位于pincontroller节点中。 第1种状态用到的引脚在pinctrl-1中定义，它是state_1_node_a，位于pincontroller节点中。 当这个设备处于default状态时，pinctrl子系统会自动根据上述信息把所用引脚复用为uart0功能。 当这这个设备处于sleep状态时，pinctrl子系统会自动根据上述信息把所用引脚配置为高电平。 groups和function 一个设备会用到一个或多个引脚，这些引脚就可以归为一组(group)；这些引脚可以复用为某个功能：function。 当然：一个设备可以用到多组引脚，比如A1、A2两组引脚，A1组复用为F1功能，A2组复用为F2功能。 Generic pin multiplexing node和Generic pin configuration node 在上图左边的pin controller节点中，有子节点或孙节点，它们是给client device使用的。 可以用来描述复用信息：哪组(group)引脚复用为哪个功能(function)； 可以用来描述配置信息：哪组(group)引脚配置为哪个设置功能(setting)，比如上拉、下拉等。 注意：pin controller节点的格式，没有统一的标准！！！！每家芯片都不一样。 甚至上面的group、function关键字也不一定有，但是概念是有的。 代码中怎么引用pinctrl 这是透明的，我们的驱动基本不用管。当设备切换状态时，对应的pinctrl就会被调用。 比如在platform_device和platform_driver的枚举过程中，流程如下： 当系统休眠时，也会去设置该设备sleep状态对应的引脚，不需要我们自己去调用代码。 非要自己调用，也有函数： devm_pinctrl_get_select_default(struct device *dev); // 使用"default"状态的引脚 pinctrl_get_select(struct device *dev, const char *name); // 根据name选择某种状态的引脚 pinctrl_put(struct pinctrl *p); // 不再使用, 退出时调用 pinctrl 使用示例 正常需要做三步： 哪个引脚？ iomux 引脚功能？ i2c 还是什么？ 引脚配置？ 开漏还是什么 设备树 引脚功能和引脚配置，会在设备树中体现, 可以参考 Linux-4.9.88\Documentation\devicetree\bindings\pinctrl\pinctrl-bindings.txt pinctrl 节点描述 pincontroller { ... /* Standard DT properties for the device itself elided */ state_0_node_a { ... }; state_1_node_a { ... }; state_1_node_b { ... }; } 设备节点描述 device { pinctrl-names = "active", "idle"; pinctrl-0 = <&state_0_node_a>; pinctrl-1 = <&state_1_node_a &state_1_node_b>; }; pinctrl 节点 生成pincontroller设备树信息，有3中方法： 有些芯片有图形化的工具，可以点点鼠标就可以配置引脚信息，得到pincontroller中的信息 有些芯片，只能看厂家给的设备树文档或者参考设备树的例子 最差的就是需要阅读驱动代码才能构造设备树信息。 设备树示例 pinctrl MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x000018B0 左边是引脚功能，右边是引脚配置 &iomuxc { pinctrl-names = "default"; pinctrl-0 = <&BOARD_InitPins>; imx6ull-board { i2c1_pins: i2c1_grp { /*!< Function assigned for the core: Cortex-A7[ca7] */ fsl,pins = < MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x000018B0 MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x000018B0 >; }; }; }; 设备 pinctrl-0 = <&i2c1_pins>; 使用的是 i2c1_pins: i2c1_grp 左边的标签。 &i2c1 { clock-frequency = <100000>; pinctrl-names = "default"; pinctrl-0 = <&i2c1_pins>; status = "okay"; }; pinctrl 主要数据结构 参考资料： Linux 5.x内核 Documentation\devicetree\bindings\pinctrl\pinctrl-bindings.txt arch/arm/boot/dts/stm32mp151.dtsi arch/arm/boot/dts/stm32mp157-100ask-pinctrl.dtsi arch/arm/boot/dts/stm32mp15xx-100ask.dtsi drivers\pinctrl\stm32\pinctrl-stm32mp157.c drivers\pinctrl\stm32\pinctrl-stm32.c Linux 4.x内核文档 Documentation\pinctrl.txt Documentation\devicetree\bindings\pinctrl\pinctrl-bindings.txt arch/arm/boot/dts/imx6ull-14x14-evk.dts arch/arm/boot/dts/100ask_imx6ull-14x14.dts drivers\pinctrl\freescale\pinctrl-imx6ul.c drivers\pinctrl\freescale\pinctrl-imx.c 匹配关系 iomuxc: iomuxc@020e0000 { compatible = "fsl,imx6ul-iomuxc"; reg = <0x020e0000 0x4000>; }; // drivers\pinctrl\freescale\pinctrl-imx6ul.c static struct of_device_id imx6ul_pinctrl_of_match[] = { { .compatible = "fsl,imx6ul-iomuxc", .data = &imx6ul_pinctrl_info, }, { .compatible = "fsl,imx6ull-iomuxc-snvs", .data = &imx6ull_snvs_pinctrl_info, }, { /* sentinel */ } }; static int imx6ul_pinctrl_probe(struct platform_device *pdev) { ... return imx_pinctrl_probe(pdev, pinctrl_info); } static struct platform_driver imx6ul_pinctrl_driver = { .driver = { .name = "imx6ul-pinctrl", .of_match_table = of_match_ptr(imx6ul_pinctrl_of_match), }, .probe = imx6ul_pinctrl_probe, }; // drivers\pinctrl\freescale\pinctrl-imx.c int imx_pinctrl_probe(struct platform_device *pdev, struct imx_pinctrl_soc_info *info) 最终调用了 imx_pinctrl_probe 这个函数。 pinctrl_desc和pinctrl_dev int imx_pinctrl_probe(struct platform_device *pdev, struct imx_pinctrl_soc_info *info) 中会配置 struct pinctrl_desc 这个结构体. int imx_pinctrl_probe(struct platform_device *pdev, struct imx_pinctrl_soc_info *info) { ... struct pinctrl_desc *imx_pinctrl_desc; ... imx_pinctrl_desc->name = dev_name(&pdev->dev); imx_pinctrl_desc->pins = info->pins; imx_pinctrl_desc->npins = info->npins; imx_pinctrl_desc->pctlops = &imx_pctrl_ops; imx_pinctrl_desc->pmxops = &imx_pmx_ops; imx_pinctrl_desc->confops = &imx_pinconf_ops; imx_pinctrl_desc->owner = THIS_MODULE; ... pinctrl_desc struct pinctrl_desc { const char *name; const struct pinctrl_pin_desc *pins; unsigned int npins; const struct pinctrl_ops *pctlops; const struct pinmux_ops *pmxops; const struct pinconf_ops *confops; struct module *owner; ... }; pinctrl_desc 中比较重要的: pins npins 这两个都是用来描述单个引脚的。pinctrl_pin_desc来描述一个引脚，一个pin controller有多个引脚。 struct pinctrl_pin_desc { unsigned number; const char *name; void *drv_data; }; pctlops 操作引脚， 来取出某组的引脚：get_groups_count、get_group_pins 处理设备树中pin controller中的某个节点：dt_node_to_map，把device_node转换为一系列的pinctrl_map struct pinctrl_ops { int (*get_groups_count) (struct pinctrl_dev *pctldev); const char *(*get_group_name) (struct pinctrl_dev *pctldev, unsigned selector); int (*get_group_pins) (struct pinctrl_dev *pctldev, unsigned selector, const unsigned **pins, unsigned *num_pins); ... int (*dt_node_to_map) (struct pinctrl_dev *pctldev, struct device_node *np_config, struct pinctrl_map **map, unsigned *num_maps); ... }; pmxops 引脚复用 struct pinmux_ops { ... int (*set_mux) (struct pinctrl_dev *pctldev, unsigned func_selector, unsigned group_selector); ... }; confops 引脚配置 struct pinconf_ops { ... int (*pin_config_get) (struct pinctrl_dev *pctldev, unsigned pin, unsigned long *config); int (*pin_config_set) (struct pinctrl_dev *pctldev, unsigned pin, unsigned long *configs, unsigned num_configs); int (*pin_config_group_get) (struct pinctrl_dev *pctldev, unsigned selector, unsigned long *config); int (*pin_config_group_set) (struct pinctrl_dev *pctldev, unsigned selector, unsigned long *configs, unsigned num_configs); ... devm_pinctrl_register 注册 pinctrl 流程： imx_pinctrl_probe -> ipctl->pctl = devm_pinctrl_register(&pdev->dev, imx_pinctrl_desc, ipctl); -> pctldev = pinctrl_register(pctldesc, dev, driver_data); struct pinctrl_dev *devm_pinctrl_register(struct device *dev, struct pinctrl_desc *pctldesc, void *driver_data) 输入 struct pinctrl_desc 返回的是 struct pinctrl_dev . pinctrl_dev struct pinctrl_dev { struct list_head node; struct pinctrl_desc *desc; struct radix_tree_root pin_desc_tree; struct list_head gpio_ranges; struct device *dev; struct module *owner; void *driver_data; struct pinctrl *p; struct pinctrl_state *hog_default; struct pinctrl_state *hog_sleep; struct mutex mutex; #ifdef CONFIG_DEBUG_FS struct dentry *device_root; #endif }; desc 指向前面构造好的 pinctrl_desc dev 指向 struct device struct device { ... struct dev_pin_info *pins; ... }; struct device 中 pins 对应的 struct dev_pin_info 包含了 pin 的一些信息 struct dev_pin_info { struct pinctrl *p; struct pinctrl_state *default_state; struct pinctrl_state *init_state; #ifdef CONFIG_PM struct pinctrl_state *sleep_state; struct pinctrl_state *idle_state; #endif }; client 设备节点设备树 对应于 client. 转化到代码中也会包含 struct device, 通过 struct dev_pin_info 来对应引脚状态。 device { pinctrl-names = "default", "sleep"; pinctrl-0 = <&state_0_node_a>; pinctrl-1 = <&state_1_node_a &state_1_node_b>; }; 这个设备树中，pinctrl-names = "default", "sleep"; 对应两种状态，default 和 sleep。 对应到 struct dev_pin_info 中的 struct pinctrl_state *default_state; 和 struct pinctrl_state *sleep_state; 正常情况下，pinctrl-names 是和 dev_pin_info 中的 default, init, sleep, idle 对应起来。 如果在 pinctrl-names 中增加一个自定义的状态，比如 plane，在 dev_pin_info 中没有直接对应，那么会在 struct pinctrl *p; 的链表中。 dev_pin_info struct dev_pin_info { struct pinctrl *p; struct pinctrl_state *default_state; struct pinctrl_state *init_state; #ifdef CONFIG_PM struct pinctrl_state *sleep_state; struct pinctrl_state *idle_state; #endif }; struct pinctrl { struct list_head node; struct device *dev; struct list_head states; struct pinctrl_state *state; struct list_head dt_maps; struct kref users; }; struct pinctrl_state { struct list_head node; const char *name; struct list_head settings; }; struct pinctrl *p; 使用链表，链接 default, init, sleep, idle, 以及自定义的状态。 struct pinctrl_state *default_state; 指向链表中的 default，其他以此类推。 default, init, sleep, idle 这几种可以通过 dev_pin_info 下面对应的指针直接访问 自定义状态，只能从 struct pinctrl *p; 来获取。 设备树关联到 pinctrl_state struct pinctrl_desc 中的 const struct pinctrl_ops *pctlops; 中的函数 int (*dt_node_to_map) (struct pinctrl_dev *pctldev, struct device_node *np_config, struct pinctrl_map **map, unsigned *num_maps); 对应到 imx_pctrl_ops.dt_node_to_map static const struct pinctrl_ops imx_pctrl_ops = { .get_groups_count = imx_get_groups_count, .get_group_name = imx_get_group_name, .get_group_pins = imx_get_group_pins, .pin_dbg_show = imx_pin_dbg_show, .dt_node_to_map = imx_dt_node_to_map, .dt_free_map = imx_dt_free_map, }; 总体的流程: 设备树中的节点 => imx_pctrl_ops.dt_node_to_map => pinctrl_map => pinctrl_setting => pinctrl_state.settings. pinctrl 设备使用哪个pin controller？ 分析设备树，找到pin controller 对于每个状态，比如default、init，去分析pin controller中的设备树节点 使用pin controller的pinctrl_ops.dt_node_to_map来处理设备树的pinctrl节点信息，得到一系列的pinctrl_map 这些pinctrl_map放在pinctrl.dt_maps链表中 每个pinctrl_map都被转换为pinctrl_setting，放在对应的pinctrl_state.settings链表中 pinctrl_map和pinctrl_setting 设备引用pin controller中的某个节点时，这个节点会被转换为一些列的pinctrl_map： 转换为多少个pinctrl_map，完全由具体的驱动决定 每个pinctrl_map，又被转换为一个pinctrl_setting 举例，设备节点里有：pinctrl-0 = <&state_0_node_a> pinctrl-0对应一个状态，会得到一个pinctrl_state state_0_node_a节点被解析为一系列的pinctrl_map 这一系列的pinctrl_map被转换为一系列的pinctrl_setting 这些pinctrl_setting被放入pinctrl_state的settings链表 使用pinctrl_setting really_probe pinctrl_bind_pins pinctrl_select_state /* Apply all the settings for the new state */ list_for_each_entry(setting, &state->settings, node) { switch (setting->type) { case PIN_MAP_TYPE_MUX_GROUP: ret = pinmux_enable_setting(setting); ret = ops->set_mux(...); break; case PIN_MAP_TYPE_CONFIGS_PIN: case PIN_MAP_TYPE_CONFIGS_GROUP: ret = pinconf_apply_setting(setting); ret = ops->pin_config_group_set(...); break; default: ret = -EINVAL; break; } Pincontroller构造过程情景分析 参考资料： Linux 4.x内核文档 Documentation\pinctrl.txt Documentation\devicetree\bindings\pinctrl\pinctrl-bindings.txt arch/arm/boot/dts/imx6ull-14x14-evk.dts arch/arm/boot/dts/100ask_imx6ull-14x14.dts drivers\pinctrl\freescale\pinctrl-imx6ul.c drivers\pinctrl\freescale\pinctrl-imx.c 源码 drivers\pinctrl\freescale\pinctrl-imx6ul.c drivers\pinctrl\freescale\pinctrl-imx.c 调用过程： imx6ul_pinctrl_probe imx_pinctrl_probe(pdev, pinctrl_info); imx_pinctrl_desc->name = dev_name(&pdev->dev); imx_pinctrl_desc->pins = info->pins; imx_pinctrl_desc->npins = info->npins; imx_pinctrl_desc->pctlops = &imx_pctrl_ops; imx_pinctrl_desc->pmxops = &imx_pmx_ops; imx_pinctrl_desc->confops = &imx_pinconf_ops; imx_pinctrl_desc->owner = THIS_MODULE; ret = imx_pinctrl_probe_dt(pdev, info); ipctl->pctl = devm_pinctrl_register(&pdev->dev, imx_pinctrl_desc, ipctl); 解析引脚 单个引脚 imx_pinctrl_desc->pins = info->pins; imx_pinctrl_desc->npins = info->npins; 开发板查看: # 查看 pinctrl 下面的 iomux, pinctrl-devices, pinctrl-maps 等 cd /sys/kernel/debug/pinctrl ls # 查看 iomuxc 下面的 pins, pinmux-pins, pinmux-functions, gpio-ranges, pinconf-config, pinconf-pins 等 cd 20e0000.iomuxc ls # 查看 pins cat pins 某组引脚 比如说 i2c 引脚可能用到一组引脚(p1.1, p1.2)，也可能用到另外一组引脚 (p2.2, p2.3) 等。 从设备树中，注明使用那组引脚。 static const struct pinctrl_ops imx_pctrl_ops = { .get_groups_count = imx_get_groups_count, .get_group_name = imx_get_group_name, .get_group_pins = imx_get_group_pins, .pin_dbg_show = imx_pin_dbg_show, .dt_node_to_map = imx_dt_node_to_map, .dt_free_map = imx_dt_free_map, }; 开发板查看: # /sys/kernel/debug/pinctrl/20e0000.iomuxc cat pingroups dtb 反汇编 dts dtc -I dtb -O dts 100ask_imx6ull-14x14.dtb > 1.dts 反汇编的 dts 中的 iomuxc 与 /sys/kernel/debug/pinctrl/20e0000.iomuxc 里面的 pingroups 是可以对应的上的。 分析 struct imx_pinctrl_soc_info 这个结构体会保存设备树中的很多信息: struct imx_pinctrl_soc_info { struct device *dev; const struct pinctrl_pin_desc *pins; unsigned int npins; struct imx_pin_reg *pin_regs; struct imx_pin_group *groups; unsigned int ngroups; unsigned int group_index; struct imx_pmx_func *functions; unsigned int nfunctions; unsigned int flags; const char *gpr_compatible; /* MUX_MODE shift and mask in case SHARE_MUX_CONF_REG */ unsigned int mux_mask; u8 mux_shift; u32 ibe_bit; u32 obe_bit; }; 使用 cat pinmux-functions 可以获得结果: function: imx6ul-evk, groups = [hoggrp-1 hdmigrp ... 这些可以对应到内核结构体 struct imx_pinctrl_soc_info 中, 即 nfunctions 为 1 只有imx6ul-evk, groups 就是一个数组，ngroups 就是这个数组的元素个数。 在设备树中，&iomuxc中的子节点个数，就是 nfunctions, 在这个子节点下面的子节点个数，就是 groups. 比如，imx6ull 中， pinctrl 的 function 对应的是 imx6ul-evk, groups 对应的是 pinctrl_hog_1、pinctrl_sii902x 等等。 fsl,pins 中就是对应具体的多个 pin, 每个pin 占用 24 字节。 如下面的设备树: &iomuxc { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_hog_1>; imx6ul-evk { pinctrl_hog_1: hoggrp-1 { fsl,pins = < MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059 /* SD1 CD */ MX6UL_PAD_GPIO1_IO00__ANATOP_OTG1_ID 0x17059 /* USB OTG1 ID */ // MX6UL_PAD_CSI_DATA07__GPIO4_IO28 0x000010B0 MX6ULL_PAD_SNVS_TAMPER5__GPIO5_IO05 0x000110A0 >; }; pinctrl_sii902x: hdmigrp { fsl,pins = < MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0x59 >; }; pinctrl_touchscreen_int: lcdif_tsc_int { fsl,pins = < MX6UL_PAD_GPIO1_IO05__GPIO1_IO05 0x000010B0 >; }; pinctrl_enet1: enet1grp { fsl,pins = < >; }; ... client端使用pinctrl过程的情景分析 参考资料： Linux 5.x内核 Documentation\devicetree\bindings\pinctrl\pinctrl-bindings.txt arch/arm/boot/dts/stm32mp151.dtsi arch/arm/boot/dts/stm32mp157-100ask-pinctrl.dtsi arch/arm/boot/dts/stm32mp15xx-100ask.dtsi drivers\pinctrl\stm32\pinctrl-stm32mp157.c drivers\pinctrl\stm32\pinctrl-stm32.c Linux 4.x内核 Documentation\pinctrl.txt Documentation\devicetree\bindings\pinctrl\pinctrl-bindings.txt arch/arm/boot/dts/imx6ull-14x14-evk.dts arch/arm/boot/dts/100ask_imx6ull-14x14.dts drivers\pinctrl\freescale\pinctrl-imx6ul.c drivers\pinctrl\freescale\pinctrl-imx.c 对应关系 上图中标签后面的 i2c1grp 对应的是 struct imx_pmx_func 中的 const char **groups; 这个 groups 是字符串数组，保存的就是这些 group 的名字。 struct imx_pmx_func { const char *name; const char **groups; unsigned num_groups; }; 数据结构 函数调用 client节点的pinctrl构造过程 really_probe pinctrl_bind_pins dev->pins = devm_kzalloc(dev, sizeof(*(dev->pins)), GFP_KERNEL); dev->pins->p = devm_pinctrl_get(dev); pinctrl_get create_pinctrl(dev); ret = pinctrl_dt_to_map(p); for_each_maps(maps_node, i, map) { ret = add_setting(p, map); } dev->pins->default_state = pinctrl_lookup_state(dev->pins->p, PINCTRL_STATE_DEFAULT); state情景分析 really_probe pinctrl_bind_pins pinctrl_select_state /* Apply all the settings for the new state */ list_for_each_entry(setting, &state->settings, node) { switch (setting->type) { case PIN_MAP_TYPE_MUX_GROUP: ret = pinmux_enable_setting(setting); ret = ops->set_mux(...); break; case PIN_MAP_TYPE_CONFIGS_PIN: case PIN_MAP_TYPE_CONFIGS_GROUP: ret = pinconf_apply_setting(setting); ret = ops->pin_config_group_set(...); break; default: ret = -EINVAL; break; } 关联 编写虚拟的Pinctrl驱动程序 框架 除了 pinctrl 驱动框架，还有对应的 client 驱动框架。 编写虚拟的Pinctrl驱动程序 设置 完善除了设备树解析以外的内容。 pin_dbg_show 和 dbg 相关的，应该是作用与 /sys/kernel/debug/pinctrl 目录。 编写虚拟的Pinctrl驱动程序 设备树 解析设备树相关 调试虚拟的Pinctrl驱动程序 Pinctrl调试信息 /sys/kernel/debug/pinctrl/目录下，每一个pin controller都有一个目录，比如virtual_pincontroller。 里面有很多文件，作用如下： Pinctrl的虚拟文件 作用 解释 pins 单个引脚信息 pingroups 引脚的组信息 pinmux-pins 单个引脚的复用信息 pinmux-functions function下的group(支持该function的group) pinconf-pins 单个引脚的配置 pinconf-groups 引脚组的配置 pinconf-config 可以通过写它修改指定设备、指定状态下、指定(组)引脚的config值 单个引脚信息 cat /sys/kernel/debug/pinctrl/virtual_pincontroller/pins registered pins: 4 pin 0 (pin0) virtual_pincontroller pin 1 (pin1) virtual_pincontroller pin 2 (pin2) virtual_pincontroller pin 3 (pin3) virtual_pincontroller 引脚的组信息 cat /sys/kernel/debug/pinctrl/virtual_pincontroller/pingroups registered pin groups: group: pin0 pin 0 (pin0) group: pin1 pin 1 (pin1) group: pin2 pin 2 (pin2) group: pin3 pin 3 (pin3) 单个引脚的复用信息 cat /sys/kernel/debug/pinctrl/virtual_pincontroller/pinmux-pins Pinmux settings per pin Format: pin (name): mux_owner gpio_owner hog? pin 0 (pin0): virtual_i2c (GPIO UNCLAIMED) function i2c group pin0 pin 1 (pin1): virtual_i2c (GPIO UNCLAIMED) function i2c group pin1 pin 2 (pin2): (MUX UNCLAIMED) (GPIO UNCLAIMED) pin 3 (pin3): (MUX UNCLAIMED) (GPIO UNCLAIMED) function下的group(支持该function的group) cat /sys/kernel/debug/pinctrl/virtual_pincontroller/pinmux-functions function: gpio, groups = [ pin0 pin1 pin2 pin3 ] function: i2c, groups = [ pin0 pin1 ] function: uart, groups = [ pin2 pin3 ] 单个引脚的配置 cat /sys/kernel/debug/pinctrl/virtual_pincontroller/pinconf-pins Pin config settings per pin Format: pin (name): configs pin 0 (pin0): 0x11223344 pin 1 (pin1): 0x55667788 pin 2 (pin2): 0x0 pin 3 (pin3): 0x0 引脚组的配置 cat /sys/kernel/debug/pinctrl/virtual_pincontroller/pinconf-groups Pin config settings per pin group Format: group (name): configs 0 (pin0): 0x11223344 1 (pin1): 0x55667788 2 (pin2): 0x0 3 (pin3): 0x0 修改配置值 内核源码： drivers\pinctrl\pinconf.c pinconf_dbg_config_write pin controller 驱动程序中的pinconf_ops提供了pin_config_dbg_parse_modify函数，就可以通过pinconf-config文件修改某个pin或某个group的配置值。 // 格式: modify <config> <devicename> <state> <pin_name|group_name> <newvalue> echo "modify config_pin virtual_i2c default pin0 0xaabb" > /sys/kernel/debug/pinctrl/virtual_pincontroller/pinconf-config cat /sys/kernel/debug/pinctrl/virtual_pincontroller/pinconf-config 总结 pinctrl 驱动加载起来之后，引脚正常会被甚至为默认的 gpio 状态。 client 驱动加载起来之后，引脚才会被配置为 i2c 等功能。
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 那一套。