mpu6050 通过 I2C 来驱动，可以使用卡尔曼滤波姿态解算。获取的传感器原始数据中，三个方向的加速度，有一个方向是 9.8 的重力加速度。 参考代码 https://blog.csdn.net/ling3ye/article/details/51360568 #include "Wire.h" #include "I2Cdev.h" #include "MPU6050.h" MPU6050 accelgyro; unsigned long now, lastTime = 0; float dt; //微分时间 int16_t ax, ay, az, gx, gy, gz; //加速度计陀螺仪原始数据 float aax=0, aay=0,aaz=0, agx=0, agy=0, agz=0; //角度变量 long axo = 0, ayo = 0, azo = 0; //加速度计偏移量 long gxo = 0, gyo = 0, gzo = 0; //陀螺仪偏移量 float pi = 3.1415926; float AcceRatio = 16384.0; //加速度计比例系数 float GyroRatio = 131.0; //陀螺仪比例系数 uint8_t n_sample = 8; //加速度计滤波算法采样个数 float aaxs[8] = {0}, aays[8] = {0}, aazs[8] = {0}; //x,y轴采样队列 long aax_sum, aay_sum,aaz_sum; //x,y轴采样和 float a_x[10]={0}, a_y[10]={0},a_z[10]={0} ,g_x[10]={0} ,g_y[10]={0},g_z[10]={0}; //加速度计协方差计算队列 float Px=1, Rx, Kx, Sx, Vx, Qx; //x轴卡尔曼变量 float Py=1, Ry, Ky, Sy, Vy, Qy; //y轴卡尔曼变量 float Pz=1, Rz, Kz, Sz, Vz, Qz; //z轴卡尔曼变量 void setup() { Wire.begin(); Serial.begin(115200); accelgyro.initialize(); //初始化 unsigned short times = 200; //采样次数 for(int i=0;i<times;i++) { accelgyro.getMotion6(&ax, &ay, &az, &gx, &gy, &gz); //读取六轴原始数值 axo += ax; ayo += ay; azo += az; //采样和 gxo += gx; gyo += gy; gzo += gz; } axo /= times; ayo /= times; azo /= times; //计算加速度计偏移 gxo /= times; gyo /= times; gzo /= times; //计算陀螺仪偏移 } void loop() { unsigned long now = millis(); //当前时间(ms) dt = (now - lastTime) / 1000.0; //微分时间(s) lastTime = now; //上一次采样时间(ms) accelgyro.getMotion6(&ax, &ay, &az, &gx, &gy, &gz); //读取六轴原始数值 float accx = ax / AcceRatio; //x轴加速度 float accy = ay / AcceRatio; //y轴加速度 float accz = az / AcceRatio; //z轴加速度 aax = atan(accy / accz) * (-180) / pi; //y轴对于z轴的夹角 aay = atan(accx / accz) * 180 / pi; //x轴对于z轴的夹角 aaz = atan(accz / accy) * 180 / pi; //z轴对于y轴的夹角 aax_sum = 0; // 对于加速度计原始数据的滑动加权滤波算法 aay_sum = 0; aaz_sum = 0; for(int i=1;i<n_sample;i++) { aaxs[i-1] = aaxs[i]; aax_sum += aaxs[i] * i; aays[i-1] = aays[i]; aay_sum += aays[i] * i; aazs[i-1] = aazs[i]; aaz_sum += aazs[i] * i; } aaxs[n_sample-1] = aax; aax_sum += aax * n_sample; aax = (aax_sum / (11*n_sample/2.0)) * 9 / 7.0; //角度调幅至0-90° aays[n_sample-1] = aay; //此处应用实验法取得合适的系数 aay_sum += aay * n_sample; //本例系数为9/7 aay = (aay_sum / (11*n_sample/2.0)) * 9 / 7.0; aazs[n_sample-1] = aaz; aaz_sum += aaz * n_sample; aaz = (aaz_sum / (11*n_sample/2.0)) * 9 / 7.0; float gyrox = - (gx-gxo) / GyroRatio * dt; //x轴角速度 float gyroy = - (gy-gyo) / GyroRatio * dt; //y轴角速度 float gyroz = - (gz-gzo) / GyroRatio * dt; //z轴角速度 agx += gyrox; //x轴角速度积分 agy += gyroy; //x轴角速度积分 agz += gyroz; /* kalman start */ Sx = 0; Rx = 0; Sy = 0; Ry = 0; Sz = 0; Rz = 0; for(int i=1;i<10;i++) { //测量值平均值运算 a_x[i-1] = a_x[i]; //即加速度平均值 Sx += a_x[i]; a_y[i-1] = a_y[i]; Sy += a_y[i]; a_z[i-1] = a_z[i]; Sz += a_z[i]; } a_x[9] = aax; Sx += aax; Sx /= 10; //x轴加速度平均值 a_y[9] = aay; Sy += aay; Sy /= 10; //y轴加速度平均值 a_z[9] = aaz; Sz += aaz; Sz /= 10; for(int i=0;i<10;i++) { Rx += sq(a_x[i] - Sx); Ry += sq(a_y[i] - Sy); Rz += sq(a_z[i] - Sz); } Rx = Rx / 9; //得到方差 Ry = Ry / 9; Rz = Rz / 9; Px = Px + 0.0025; // 0.0025在下面有说明... Kx = Px / (Px + Rx); //计算卡尔曼增益 agx = agx + Kx * (aax - agx); //陀螺仪角度与加速度计速度叠加 Px = (1 - Kx) * Px; //更新p值 Py = Py + 0.0025; Ky = Py / (Py + Ry); agy = agy + Ky * (aay - agy); Py = (1 - Ky) * Py; Pz = Pz + 0.0025; Kz = Pz / (Pz + Rz); agz = agz + Kz * (aaz - agz); Pz = (1 - Kz) * Pz; /* kalman end */ Serial.print(agx);Serial.print(","); Serial.print(agy);Serial.print(","); Serial.print(agz);Serial.println(); } 参考 MPU-6050寄存器手册中文版 MPU-60X0寄存器中文版V4.0.pdf Arduino uno + mpu6050 陀螺仪 运用卡尔曼滤波姿态解算实验 Arduino uno + mpu6050 陀螺仪 运用卡尔曼滤波姿态解算实验 MPU6050
TCP/IP 协议族 体系结构及主要协议 ARP 用于 IP 转化为物理地址；RARP 用于物理地址转 IP，适合无盘网络，只能通过自身网卡获取自身IP。 ICMP 格式: 0-7 类型，8-15 代码，16-31 校验和。 ICMP 两大类，一类是回应网络错误，一类是查询报文，比如 ping 可以查看 /etc/services 有所有知名的应用层协议。 封装 tcp 发送过程：send 向 tcp 连接写入数据 -> 内核把数据复制到连接对应的 tcp 内核发送缓冲区 --> 调用 ip 模块服务 udp 不需要为应用层保存副本，发送成功后， udp 内核缓冲区就会丢弃数据报。 帧的最大传输单元 MTU，格式： 6字节 目的物理地址，6字节 源物理地址，2字节 类型，N字节 数据，4字节 CRC。 ARP 报文：发送端不需要填写目的以太网地址。
jni 如果直接把源文件从 c 改为 cpp, 那么会报错 base operand of '->' has non-pointer type 'JNIEnv {aka _JNIEnv}'和Method 'GetStringUTFChars' could not be resolved 具体的写法是有区别的。 c 调用 const char * file_path = (*env)->GetStringUTFChars(env, path, NULL); return (*env)->NewStringUTF(env, "Hello from JNI !");//如果是用C语言格式就用这种方式 cpp 调用 const char * file_path = env->GetStringUTFChars(path, NULL); return env->NewStringUTF((char *)"Hello from JNI !");//C++用这种格式 原因是 jni 的头文件定义 c const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*); cpp const char* GetStringUTFChars(jstring string, jboolean* isCopy) { return functions->GetStringUTFChars(this, string, isCopy); } 参考 https://blog.csdn.net/hejia729371286/article/details/51356983
总结 数据采集流程 设置好长宽之后，摄像头可能不支持，需要检查长宽参数是否被修改。 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，就对电源引脚供电。
1. createTrackbar是Opencv中的API，其可在显示图像的窗口中快速创建一个滑动控件，用于手动调节阈值，具有非常直观的效果。具体定义如下： CV_EXPORTS int createTrackbar(const string& trackbarname, const string& winname, int* value, int count, TrackbarCallback onChange = 0, void* userdata = 0); 形式参数一、trackbarname：滑动空间的名称； 形式参数二、winname：滑动空间用于依附的图像窗口的名称； 形式参数三、value：初始化阈值； 形式参数四、count：滑动控件的刻度范围； 形式参数五、TrackbarCallback是回调函数，其定义如下： typedef void (CV_CDECL *TrackbarCallback)(int pos, void* userdata); 参考： https://blog.csdn.net/mysee1989/article/details/41379817 https://docs.opencv.org/3.4.1/dc/dfa/Morphology_1_8cpp-example.html#a20 2. Rect 是矩形类，成员变量x、y、width、height，分别为左上角点的坐标和矩形的宽和高。常用的成员函数有Size()返回值为一个Size，area()返回矩形的面积，contains(Point)用来判断点是否在矩形内，inside(Rect)函数判断矩形是否在该矩形内，tl()返回左上角点坐标，br()返回右下角点坐标。 Rect类的使用 rect = rect ± point (shifting a rectangle by a certain offset) rect = rect ± size (expanding or shrinking a rectangle by a certain amount) rect += point, rect -= point, rect += size, rect -= size (augmenting operations) rect = rect1 & rect2 (rectangle intersection) rect = rect1 | rect2 (minimum area rectangle containing rect1 and rect2 ) rect &= rect1, rect |= rect1 (and the corresponding augmenting operations) rect == rect1, rect != rect1 (rectangle comparison) 参考： https://www.cnblogs.com/happyamyhope/p/9844629.html 3. copyTo 是深拷贝，但是要根据大小信息，决定是否重新申请空间，clone 不管大小信息，全部重新申请空间进行深拷贝。 参考： https://blog.csdn.net/u013806541/article/details/70154719 4. opencv 提供的字符串格式化如下： string formated_str = format("I have made %d dollars on this product.", 500); 参考： https://blog.csdn.net/yiyeshuanglinzui/article/details/108388683 5. 获取矩阵的行，列，位数。 Mat(int rows, int cols, int type)， 直接使用属性就行。 m.rows m.cols 参考： https://blog.csdn.net/renweiyi1487/article/details/101616758 6. 添加文字， putText void cv::putText ( InputOutputArray img, const String & text, Point org, int fontFace, double fontScale, Scalar color, int thickness = 1, int lineType = LINE_8, bool bottomLeftOrigin = false ) cv::putText(image, text, origin, font_face, font_scale, cv::Scalar(0, 255, 255), thickness, 8, 0); 参考：https://docs.opencv.org/3.4/d6/d6e/group__imgproc__draw.html#ga5126f47f883d730f633d74f07456c576 https://blog.csdn.net/guduruyu/article/details/68491211 7. imutils 有 c++ 的部分实现，https://github.com/minooei/imutils 8. 获得轮廓最小矩形，使用 boundingRect Rect boundRect = boundingRect( contours_poly[i] ); 参考： https://docs.opencv.org/3.4/da/d0c/tutorial_bounding_rects_circles.html 9. 矩形 rectangle() void cv::rectangle ( InputOutputArray img, Point pt1, Point pt2, const Scalar & color, int thickness = 1, int lineType = LINE_8, int shift = 0 ) void cv::rectangle ( Mat & img, Rect rec, const Scalar & color, int thickness = 1, int lineType = LINE_8, int shift = 0 ) rectangle(src, boundRect[i], Scalar(0, 255, 0)); rectangle(flipFrame, Point(roi_ws, roi_hs), Point(roi_we, roi_he), Scalar(0, 255, 0), 0) 参考： https://docs.opencv.org/3.4/d6/d6e/group__imgproc__draw.html#ga07d2f74cadcf8e305e810ce8eed13bc9 https://kknews.cc/code/66yekj3.html 10. 获得轮廓最小面积 ◆ contourArea() double cv::contourArea ( InputArray contour, bool oriented = false ) fabs(contourArea(Mat(c))); 参考： https://docs.opencv.org/master/d3/dc0/group__imgproc__shape.html#ga2c759ed9f497d4a618048a2f56dc97f1 https://docs.opencv.org/3.4.1/dd/d9d/segment_objects_8cpp-example.html#a1 11. 膨胀 ◆ dilate() void cv::dilate ( InputArray src, OutputArray dst, InputArray kernel, Point anchor = Point(-1,-1), int iterations = 1, int borderType = BORDER_CONSTANT, const Scalar & borderValue = morphologyDefaultBorderValue() ) dilate(mid_filer,gray_dilate1,element); 参考： https://docs.opencv.org/3.4/d4/d86/group__imgproc__filter.html#ga4ff0f3318642c4f469d0e11f242f3b6c https://www.itread01.com/articles/1478557515.html https://docs.opencv.org/3.4.1/d8/dc0/morphology2_8cpp-example.html#a10 https://zhuanlan.zhihu.com/p/40326127 https://www.jianshu.com/p/ee72f5215e07 https://www.cnblogs.com/ssyfj/p/9276999.html 12. 查找轮廓 findContours(image,contours,hierarchy,RETR_TREE,CHAIN_APPROX_SIMPLE,Point()); 查找轮廓的参数导致的结果，参考： https://blog.csdn.net/dcrmg/article/details/51987348 13. mat 的创建，复制和释放，构造函数等等 参考： https://blog.csdn.net/wanggao_1990/article/details/53150926 https://blog.csdn.net/guyuealian/article/details/70159660 14. createTrackbar 用于创建滑动控件，方便调试效果。 参考： https://blog.csdn.net/u013270326/article/details/72821149 15. 求差 absdiff absdiff(frameNow,framePre,frameDet); 参考： https://blog.csdn.net/dcrmg/article/details/52234929 16. 翻转图像 flip() void cv::flip ( InputArray src, OutputArray dst, int flipCode ) 参考： https://docs.opencv.org/3.4/d2/de8/group__core__array.html#gaca7be533e3dac7feb70fc60635adf441 17. 截取部分矩阵 ◆ Mat() [15/29] cv::Mat::Mat ( const Mat & m, const Range & rowRange, const Range & colRange = Range::all() ) roiFrame = Mat(flipFrame, Range(roi_hs, roi_he), Range(roi_ws, roi_we)); 参考： https://docs.opencv.org/master/d3/d63/classcv_1_1Mat.html#a92a3e9e5911a2eb0cf0950a0a9670c76 18. 转换颜色 ◆ cvtColor() void cv::cvtColor ( InputArray src, OutputArray dst, int code, int dstCn = 0 ) cvtColor(roiFrame, hsvFrame, COLOR_BGR2HSV) 参考： https://docs.opencv.org/3.4/d8/d01/group__imgproc__color__conversions.html#ga397ae87e1288a81d2363b61574eb8cab 19. 通过上下限阈值从图像中提取作为前景，其他作为后景，完成图像的二值化。 inRange() void cv::inRange ( InputArray src, InputArray lowerb, InputArray upperb, OutputArray dst ) inRange(hsvFrame, Scalar(0, 20, 70), Scalar(20, 255, 255), maskFrame); 参考： https://docs.opencv.org/master/d2/de8/group__core__array.html#ga48af0ab51e36436c5d04340e036ce981 https://docs.opencv.org/master/d6/d7f/samples_2cpp_2camshiftdemo_8cpp-example.html#a33 20. 寻找轮廓 findContours 获得的轮廓类型是： std::vector<std::vector >, 获取轮廓面积的时候输入的轮廓参数是 std::vector 参考： https://docs.opencv.org/master/d3/dc0/group__imgproc__shape.html#gadf1ad6a0b82947fa1fe3c3d497f260e0 https://docs.opencv.org/master/d3/dc0/group__imgproc__shape.html#ga2c759ed9f497d4a618048a2f56dc97f1 21. 画轮廓线 ◆ drawContours() void cv::drawContours ( InputOutputArray image, InputArrayOfArrays contours, int contourIdx, const Scalar & color, int thickness = 1, int lineType = LINE_8, InputArray hierarchy = noArray(), int maxLevel = INT_MAX, Point offset = Point() ) 参考： https://docs.opencv.org/master/d6/d6e/group__imgproc__draw.html#ga746c0625f1781f1ffc9056259103edbc 22. 计算曲线长度或者轮廓周长。 ◆ arcLength() double cv::arcLength ( InputArray curve, bool closed ) 参考： https://docs.opencv.org/master/d3/dc0/group__imgproc__shape.html#ga8d26483c636be6b35c3ec6335798a47c 23. 计算轮廓近似多边形 ◆ approxPolyDP() void cv::approxPolyDP ( InputArray curve, OutputArray approxCurve, double epsilon, bool closed ) 参考： https://docs.opencv.org/master/d3/dc0/group__imgproc__shape.html#ga0012a5fdaea70b8a9970165d98722b4c 24. 计算凸包 ◆ convexHull() void cv::convexHull ( InputArray points, OutputArray hull, bool clockwise = false, bool returnPoints = true ) 参考： https://docs.opencv.org/master/d3/dc0/group__imgproc__shape.html#ga014b28e56cb8854c0de4a211cb2be656 25. 计算凸性缺陷 ◆ convexityDefects() void cv::convexityDefects ( InputArray contour, InputArray convexhull, OutputArray convexityDefects ) 注意： contour 是 vector， convexhull 是 vector，convexityDefects 是 vector。 如果有报错： OpenCV Error: Assertion failed (hpoints > 0) in convexityDefects, file /home/neha/opencv-3.4.0/modules/imgproc/src/convhull.cpp, line 284 terminate called after throwing an instance of 'cv::Exception' what(): /home/neha/opencv-3.4.0/modules/imgproc/src/convhull.cpp:284: error: (-215) hpoints > 0 in function convexityDefects 那么需要检查 convexhull 是不是 vector。 参考： https://docs.opencv.org/master/d3/dc0/group__imgproc__shape.html#gada4437098113fd8683c932e0567f47ba https://github.com/wonderseen/Sparse-Points-Gen-Convex/issues/1 26. Vec4i 是 4个 int 组成的 vector 向量，即 vector 参考： https://www.coder.work/article/826469 27. 画圆圈。 ◆ circle() void cv::circle ( InputOutputArray img, Point center, int radius, const Scalar & color, int thickness = 1, int lineType = LINE_8, int shift = 0 ) 参考： https://docs.opencv.org/master/d6/d6e/group__imgproc__draw.html#gaf10604b069374903dbd0f0488cb43670 https://blog.csdn.net/caomin1hao/article/details/81876836 28. 画线 ◆ line() void cv::line ( InputOutputArray img, Point pt1, Point pt2, const Scalar & color, int thickness = 1, int lineType = LINE_8, int shift = 0 ) 参考： https://docs.opencv.org/master/d6/d6e/group__imgproc__draw.html#ga7078a9fae8c7e7d13d24dac2520ae4a2 29. 高斯模糊 ◆ GaussianBlur() void cv::GaussianBlur ( InputArray src, OutputArray dst, Size ksize, double sigmaX, double sigmaY = 0, int borderType = BORDER_DEFAULT ) 模糊参数的影响效果可以参考： https://www.cnblogs.com/sdu20112013/p/11600436.html 参考： https://docs.opencv.org/master/d4/d86/group__imgproc__filter.html#gaabe8c836e97159a9193fb0b11ac52cf1 30. 手势识别，用到凸包和缺陷。 具体可参考： http://www.zfhblog.com/index.php/archives/22/ https://www.cnblogs.com/Anita9002/p/5332122.html https://docs.opencv.org/master/d7/d1d/tutorial_hull.html https://blog.csdn.net/lichengyu/article/details/38392473 31. resize cv::Mat dst(300, 300, image.type()); cv::resize(image, dst, dst.size(), 0, 0, cv::INTER_LINEAR); 参考： https://blog.csdn.net/i_chaoren/article/details/54564663 https://blog.csdn.net/u012005313/article/details/51943442 https://www.jianshu.com/p/11879a49d1a0 32. 复制所有的图像数据 cv::Mat image; image = cv::imread(imgPath); memcpy(data, image.data, image.dataend - image.datastart); 参考: https://www.jianshu.com/p/cfc0c1f87bf8 33. 清除图像数据 Mat mat3 = Mat::zeros(1, 4, CV_32F); mat3.release(); 参考: https://blog.csdn.net/wanggao_1990/article/details/53150926
总结 整体 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 这样的结构体。