串口是很常用的一个外设,在 Linux 下通常通过串口和其他设备或传感器进行通信,根据电平的不同,串口分为 TTL 和 RS232。不管是什么样的接口电平,其驱动程序都是一样的,通过外接 RS485 这样的芯片就可以将串口转换为 RS485 信号,正点原子的 I.MX6U-ALPHA 开发板就是这么做的。对于正点原子的 I.MX6U-ALPHA 开发板而言, RS232、RS485 以及 GPS 模块接口通通连接到了 I.MX6U 的 UART3 接口上,因此这些外设最终都归结为 UART3 的串口驱动。本章我们就来学习一下如何驱动 I.MX6U-ALPHA 开发板上的 UART3 串口。
Linux 下 UART 驱动框架
1、uart_driver 注册与注销
同 I2C、SPI 一样,Linux 也提供了串口驱动框架,我们只需要按照相应的串口框架编写驱动程序即可。串口驱动没有什么主机端和设备端之分,就只有一个串口驱动,而且这个驱动也已经由 NXP 官方已经编写好了,我们真正要做的就是在设备树中添加所要使用的串口节点信息。当系统启动以后串口驱动和设备匹配成功,相应的串口就会被驱动起来,生成/dev/ttymxcX(X=0….n)文件。
虽然串口驱动不需要我们去写,但是串口驱动框架我们还是需要了解的,uart_driver 结构体表示 UART 驱动,uart_driver 定义在 include/linux/serial_core.h 文件中,内容如下:
每个串口驱动都需要定义一个 uart_driver,加载驱动的时候通过 uart_register_driver 函数向系统注册这个 uart_driver,此函数原型如下:
int uart_register_driver(struct uart_driver *drv)
函数参数和返回值含义如下:
drv:要注册的 uart_driver。
返回值:0,成功;负值,失败。
注销驱动的时候也需要注销掉前面注册的 uart_driver,需要用到 uart_unregister_driver 函数,
函数原型如下:
void uart_unregister_driver(struct uart_driver *drv)
函数参数和返回值含义如下:
drv:要注销的 uart_driver。
返回值:无。
2、uart_port 的添加与移除
uart_port 表示一个具体的 port,uart_port 定义在 include/linux/serial_core.h 文件,内容如下 (有省略):
uart_port 中最主要的就是第 235 行的 ops,ops 包含了串口的具体驱动函数,这个我们稍后再看。每个 UART 都有一个 uart_port,那么 uart_port 是怎么和 uart_driver 结合起来的呢?这里要用到 uart_add_one_port 函数,函数原型如下:
int uart_add_one_port(struct uart_driver *drv, struct uart_port *uport)
函数参数和返回值含义如下:
drv:此 port 对应的 uart_driver。
uport:要添加到 uart_driver 中的 port。
返回值:0,成功;负值,失败。
卸载 UART 驱动的时候也需要将 uart_port 从相应的 uart_driver 中移除,需要用到
uart_remove_one_port 函数,函数原型如下:
int uart_remove_one_port(struct uart_driver *drv, struct uart_port *uport)
函数参数和返回值含义如下:
drv:要卸载的 port 所对应的 uart_driver。
uport:要卸载的 uart_port。
返回值:0,成功;负值,失败。
3、uart_ops 实现
在上面讲解 uart_port 的时候说过,uart_port 中的 ops 成员变量很重要,因为 ops 包含了针对 UART 具体的驱动函数,Linux 系统收发数据最终调用的都是 ops 中的函数。ops 是 uart_ops类型的结构体指针变量,uart_ops 定义在 include/linux/serial_core.h 文件中,内容如下:
UART 驱动编写人员需要实现 uart_ops,因为 uart_ops 是最底层的 UART 驱动接口,是实实在在的和 UART 寄存器打交道的。关于 uart_ops 结构体中的这些函数的具体含义请参考 Documentation/serial/driver 这个文档。
UART 驱动框架大概就是这些,接下来我们理论联系实际,看一下 NXP 官方的 UART 驱动文件是如何编写的。
I.MX6U UART 驱动分析
1、UART 的 platform 驱动框架
打开 imx6ull.dtsi 文件,找到 UART3 对应的子节点,子节点内容如下所示:
重点看一下第 2,3 行的 compatible 属性,这里一共有三个值:“fsl,imx6ul-uart”、“fsl,imx6q-uart”和“fsl,imx21-uart”。在 linux 源码中搜索这三个值即可找到对应的 UART 驱动文件,此文件为 drivers/tty/serial/imx.c,在此文件中可以找到如下内容:
可以看出 I.MX6U 的 UART 本质上是一个 platform 驱动,第 267~280 行,imx_uart_devtype为传统匹配表。
第 283~288 行,设备树所使用的匹配表,第 284 行的 compatible 属性值为“fsl,imx6q-uart”。
第 2071~2082 行,platform 驱动框架结构体 serial_imx_driver。
第 2084~2096 行,驱动入口函数,第 2086 行调用 uart_register_driver 函数向 Linux 内核注册 uart_driver,在这里就是 imx_reg。
第 2098~2102 行,驱动出口函数,第 2101 行调用 uart_unregister_driver 函数注销掉前面注册的 uart_driver,也就是 imx_reg。
2、uart_driver 初始化
在 imx_serial_init 函数中向 Linux 内核注册了 imx_reg,imx_reg 就是 uart_driver 类型的结构体变量,imx_reg 定义如下:
3、uart_port 初始化与添加
当 UART 设备和驱动匹配成功以后 serial_imx_probe 函数就会执行,此函数的重点工作就是初始化 uart_port,然后将其添加到对应的 uart_driver 中。在看 serial_imx_probe 函数之前先来看一下 imx_port 结构体,imx_port 是 NXP 为 I.MX 系列 SOC 定义的一个设备结构体,此结构体内部就包含了 uart_port 成员变量,imx_port 结构体内容如下所示(有缩减):
第 217 行,uart_port 成员变量 port。
接下来看一下 serial_imx_probe 函数,函数内容如下:
第 1971 行,定义一个 imx_port 类型的结构体指针变量 sport。
第 1977 行,为 sport 申请内存。
第 1987~1988 行,从设备树中获取 I.MX 系列 SOC UART 外设寄存器首地址,对于I.MX6ULL 的 UART3 来说就是 0X021EC000。得到寄存器首地址以后对其进行内存映射,得到对应的虚拟地址。
第 1992~1994 行,获取中断信息。
第 1996~2034 行,初始化 sport,我们重点关注的就是第 2003 行初始化 sport 的 port 成员变量,也就是设置 uart_ops 为 imx_pops,imx_pops 就是 I.MX6ULL 最底层的驱动函数集合,稍后再来看。
第 2040~2055 行,申请中断。
第 2061 行,使用 uart_add_one_port 向 uart_driver 添加 uart_port,在这里就是向 imx_reg 添加 sport->port。
4、imx_pops 结构体变量
imx_pops 就是 uart_ops 类型的结构体变量,保存了 I.MX6ULL 串口最底层的操作函数,imx_pops 定义如下:
imx_pops 中的函数基本都是和 I.MX6ULL 的 UART 寄存器打交道的,这里就不去详细的分析了。简单的了解了 I.MX6U 的 UART 驱动以后我们再来学习一下,如何驱动正点原子I.MX6U-ALPHA 开发板上的 UART3 接口。
RS232 驱动编写
前面我们已经说过了,I.MX6U 的 UART 驱动 NXP 已经编写好了,所以不需要我们编写。
我们要做的就是在设备树中添加 UART3 对应的设备节点即可。打开 imx6ull-alientek-emmc.dts文件,在此文件中只有 UART1 对应的 uart1 节点,并没有 UART3 对应的节点,因此我们可以参考 uart1 节点创建 uart3 节点。
1、UART3 IO 节点创建
UART3 用到了UART3_TXD 和UART3_RXD 这两个IO,因此要先在 iomuxc 中创建UART3对应的 pinctrl 子节点,在 iomuxc 中添加如下内容:
最后检查一下 UART3_TX 和 UART3_RX 这两个引脚有没有被用作其他功能,如果有的话要将其屏蔽掉,保证这两个 IO 只用作 UART3,切记!!!
2、添加 uart3 节点
默认情况下 imx6ull-alientek-emmc.dts 中只有 uart1 和 uart2 这两个节点,如图 63.4.1 所示:
uart1 是 UART1 的,在正点原子的 I.MX6U-ALPHA 开发板上没有用到 UART2,而且 UART2默认用到了 UART3 的 IO,因此需要将 uart2 这个节点删除掉,然后加上 UART3 对应的 uart3,uart3 节点内容如下:
完成以后重新编译设备树并使用新的设备树启动 Linux,如果设备树修改成功的话,系统启动以后就会生成一个名为“/dev/ttymxc2”的设备文件,ttymxc2 就是 UART3 对应的设备文件,应用程序可以通过访问 ttymxc2 来实现对 UART3 的操作。
补充
为什么串口的操作函数是struct uart_ops 而不是struct file_operations
串口(UART,Universal Asynchronous Receiver/Transmitter)驱动的操作函数使用
struct uart_ops
而不是struct file_operations
,主要是因为串口驱动的设计需要满足以下需求:1. 串口驱动的分层设计
串口驱动在内核中分为两层:
上层:与用户空间交互的部分,使用
struct file_operations
。下层:与硬件交互的部分,使用
struct uart_ops
。这种分层设计使得串口驱动可以更好地分离硬件操作和用户接口,提高代码的模块化和可维护性。
2.
struct uart_ops
的作用
struct uart_ops
是串口驱动与硬件交互的核心结构体,它定义了串口控制器硬件的操作函数。这些函数包括:启动和停止串口传输。
配置串口参数(如波特率、数据位、停止位等)。
读取和写入数据。
处理中断。
这些操作是硬件相关的,通常由串口控制器驱动实现。
3.
struct file_operations
的作用
struct file_operations
是字符设备驱动的标准接口,用于与用户空间交互。对于串口设备,struct file_operations
的实现通常由 TTY 子系统提供,而不是由具体的串口驱动实现。TTY 子系统是 Linux 内核中用于管理终端设备的框架,它负责:
提供统一的用户接口(如
/dev/ttyS0
)。处理用户空间的系统调用(如
read
、write
、ioctl
等)。将用户空间的操作转换为对底层串口驱动的调用。
4. 为什么串口驱动不直接使用
struct file_operations
硬件操作与用户接口分离:
串口驱动需要处理硬件相关的操作(如配置波特率、处理中断等),这些操作与用户空间的接口无关。
通过
struct uart_ops
,串口驱动可以专注于硬件操作,而用户接口由 TTY 子系统统一管理。复用 TTY 子系统的功能:
TTY 子系统已经实现了与用户空间交互的大部分功能(如行规范、缓冲、信号处理等)。
如果串口驱动直接使用
struct file_operations
,就需要重新实现这些功能,导致代码冗余。支持多种设备类型:
TTY 子系统不仅支持串口设备,还支持伪终端(PTY)、控制台(Console)等设备。
通过
struct uart_ops
,串口驱动可以无缝集成到 TTY 子系统中,与其他设备共享相同的用户接口。5. 串口驱动的工作流程
用户空间操作:
用户程序通过
/dev/ttyS0
等设备文件访问串口。
系统调用(如
open
、read
、write
)由 TTY 子系统处理。TTY 子系统:
TTY 子系统调用底层串口驱动的
struct uart_ops
函数来完成硬件操作。
例如,当用户调用
write
时,TTY 子系统会调用struct uart_ops
中的start_tx
函数来启动数据传输。串口驱动:
串口驱动实现
struct uart_ops
中的函数,直接操作硬件。
例如,
start_tx
函数会将数据写入串口控制器的发送寄存器。