R Field - c
type: channel
link: http://www.ramlife.org/tag/c/
items: mpu6050 使用
items: Linux 高性能服务器编程 学习笔记
items: kernel 延时
items: jni 使用 c++ 和 c 不同的格式
items: 100ask 韦东山 驱动大全 子系统 v4l2 学习笔记
items: 100ask 韦东山 驱动大全 子系统 usb 学习笔记
items: opencv c 学习笔记
items: 100ask 韦东山 驱动大全 子系统 spi 学习笔记
items: 100ask 韦东山 驱动大全 子系统 pci 学习笔记
items: 100ask 韦东山 驱动大全 子系统 uart 学习笔记

mpu6050 使用
type: item
date: 2025-04-29T18:47:00+08:00
description: 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 += …

Linux 高性能服务器编程 学习笔记
type: item
date: 2024-06-01T10:20:00+08:00
description: 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 报文：发送端不需要填写目的以太网地址。

kernel 延时
type: item
date: 2022-04-28T11:35:00+08:00
description: 参考: udelay、mdelay、ndelay、msleep使用比较说明 https://www.cnblogs.com/Ph-one/p/4678361.html arm体系下的cpu_relax()函数疑惑 http://www.wowotech.net/forum/viewtopic.php?id=21#:~:text=cpu_relax%E5%BF%85%E9%A1%BB%E5%85%B7%E5%A4%87%E4%B8%A4%E4%B8%AA%E5%8A%9F%E8%83%BD%EF%BC%9A%201%E3%80%81%E7%A1%AE%E4%BF%9D%E5%AF%B9flag%E7%9A%84%E8%AE%BF%E9%97%AE%E6%AF%8F%E6%AC%A1%E9%83%BD%E4%BB%8Ememory%E4%B8%AD%E5%8A%A0%E8%BD%BD%EF%BC%8C%E4%B9%9F%E5%B0%B1%E6%98%AFbarrier,%28%29%E5%87%BD%E6%95%B0%E7%9A%84%E4%BD%9C%E7%94%A8%202%E3%80%81%E9%80%9A%E7%9F%A5%E5%BA%95%E5%B1%82CPU%EF%BC%8C%E7%9B%AE%E5%89%8D%E4%BB%A3%E7%A0%81%E6%B2%A1%E6%9C%89%E5%9C%A8%E5%81%9A%E4%BB%80%E4%B9%88%E5%AE%9E%E9%99%85%E6%9C%89%E6%84%8F%E4%B9%89%E7%9A%84%E4%BA%8B%E6%83%85%EF%BC%8C%E5%A6%82%E6%9E%9C%E5%8F%AF%E4%BB%A5%E7%9A%84%E8%AF%9D%EF%BC%8C%E5%88%AB%E8%AE%A9cpu%E5%81%9A%E5%A4%AA%E5%A4%9A%E4%BA%8B%E6%83%85%EF%BC%8C%E7%B3%BB%E7%BB%9F%E7%9A%84%E8%B5%84%E6%BA%90%E5%B0%BD%E9%87%8F%E8%AE%A9%E7%BB%99%E5%85%B6%E4%BB%96%E7%9A%84cpu%E3%80%82

jni 使用 c++ 和 c 不同的格式
type: item
date: 2021-08-20T21:50:00+08:00
description: 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

100ask 韦东山 驱动大全 子系统 v4l2 学习笔记
type: item
date: 2020-12-13T13:45:00+08:00
description: 总结 数据采集流程 设置好长宽之后，摄像头可能不支持，需要检查长宽参数是否被修改。 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; } } …

100ask 韦东山 驱动大全 子系统 usb 学习笔记
type: item
date: 2020-11-29T09:45:00+08:00
description: 总结 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 鼠标…

opencv c 学习笔记
type: item
date: 2020-11-20T14:24:00+08:00
description: 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…

100ask 韦东山 驱动大全 子系统 spi 学习笔记
type: item
date: 2020-11-15T15:10:00+08:00
description: 总结 整体 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 休…

100ask 韦东山 驱动大全 子系统 pci 学习笔记
type: item
date: 2020-11-01T09:30:00+08:00
description: 总结 pci 和 pcie 都是对地址进行转换，转换情况需要上电后，写到配置寄存器中。一般这种中间设备成为 pci 桥，相关驱动是 pci_host. pci 通过地址线的方式，来选通桥后的设备。 pcie桥本身内部端口有地址，端口后只接一个设备，选通的其实是端口。 针对通过本级 pci/pcie 能够直达的，配置是，发送 type0包，不能直达的，发送 type1包。 type1包在桥之间转发，直到下级就是目标终端，才会转换为 type0 包。 数据包中，一般包含 bus, device, function, register 四部分。 桥设备会根据直接连接的下级设备，向上级索要地址范围。

100ask 韦东山 驱动大全 子系统 uart 学习笔记
type: item
date: 2020-10-18T17:30:00+08:00
description: 总结 分层 主要分为三层， 最下面的厂商硬件相关驱动 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 这样的结构体。