47.Linux UART 驱动
串口是很常用的一个外设,在 Linux 下通常通过串口和其他设备或传感器进行通信,根据电平的不同,串口分为 TTL 和 RS232。不管是什么样的接口电平,其驱动程序都是一样的,通过外接 RS485 这样的芯片就可以将串口转换为 RS485 信号,正点原子的 I.MX6U-ALPHA 开发板就是这么做的。对于正点原子的 I.MX6U-ALPHA 开发板而言, RS232、 RS485 以及 GPS 模块接口通通连接到了 I.MX6U 的 UART3 接口上,因此这些外设最终都归结为 UART3 的串口驱动。本章我们就来学习一下如何驱动 I.MX6U-ALPHA 开发板上的 UART3 串口,进而实现 RS232、RS485 以及 GSP 驱动。
在内核中的体现:
c
/dev/ttymcx0 -> uart1
/dev/ttymcx1 -> uart2
/dev/ttymcx2 -> uart3
最重要的是uart_driver和uart_port这两个结构体
uart_driver
uart_driver 结构体表示 UART 驱动, uart_driver 定义在 include/linux/serial_core.h 文件中,内容如下:
c
struct uart_driver {
struct module *owner;
const char *driver_name; /* 驱动名字 */
const char *dev_name; /* 设备名字 */
int major;
int minor;
int nr; /* 设备数 */
struct console *cons; /* 控制台 */
/*
* these are private; the low level driver should not
* touch these; they should be initialised to NULL
*/
struct uart_state *state;
struct tty_driver *tty_driver;
};
注册与注销
每个串口驱动都需要定义一个 uart_driver,加载驱动的时候通过 uart_register_driver 函数向系统注册这个 uart_driver,此函数原型如下:
c
int uart_register_driver(struct uart_driver *drv)
drv: 要注册的 uart_driver。
返回值: 0,成功;负值,失败。
注销驱动的时候也需要注销掉前面注册的 uart_driver,需要用到 uart_unregister_driver 函数,函数原型如下:
c
void uart_unregister_driver(struct uart_driver *drv)
函数参数和返回值含义如下: drv: 要注销的 uart_driver。
返回值: 无。
2、 uart_port 的添加与移除
uart_port 表示一个具体的 port,如同不能i2c_device、spi_device. uart_port 定义在 include/linux/serial_core.h 文件,内容如下(有省略):
c
struct uart_port {
spinlock_t lock; /* port lock */
unsigned long iobase; /* in/out[bwl] */
unsigned char __iomem *membase; /* read/write[bwl] */
...
const struct uart_ops *ops;
unsigned int custom_divisor;
unsigned int line; /* port index */
unsigned int minor;
resource_size_t mapbase; /* for ioremap */
resource_size_t mapsize;
struct device *dev; /* parent device */
...
}
const struct uart_ops *ops;是其中最重要的参数,ops包含了串口的具体驱动函数,
每个 UART 都有一个 uart_port,那么 uart_port 是怎么和 uart_driver 结合起来的呢?这里要用到 uart_add_one_port 函数,函数原型如下:
c
int uart_add_one_port(struct uart_driver *reg, struct uart_port *port);
drv:此 port 对应的 uart_driver。
uport: 要添加到 uart_driver 中的 port。
返回值: 0,成功;负值,失败。
卸载 UART 驱动的时候也需要将 uart_port 从相应的 uart_driver 中移除,需要用到uart_remove_one_port 函数,函数原型如下:
c
int uart_remove_one_port(struct uart_driver *reg, struct uart_port *port);
uart_ops
const struct uart_ops *ops;是uart_port最重要的参数
因为 ops 包含了针对 UART 具体的驱动函数, Linux 系统收发数据最终调用的都是 ops 中的函数。 ops 是 uart_ops类型的结构体指针变量, uart_ops 定义在 include/linux/serial_core.h 文件中,内容如下:
c
/*
* This structure describes all the operations that can be done on the
* physical hardware. See Documentation/serial/driver for details.
*/
struct uart_ops {
unsigned int (*tx_empty)(struct uart_port *);
void (*set_mctrl)(struct uart_port *, unsigned int mctrl);
unsigned int (*get_mctrl)(struct uart_port *);
void (*stop_tx)(struct uart_port *);
void (*start_tx)(struct uart_port *);
void (*throttle)(struct uart_port *);
void (*unthrottle)(struct uart_port *);
void (*send_xchar)(struct uart_port *, char ch);
void (*stop_rx)(struct uart_port *);
void (*enable_ms)(struct uart_port *);
void (*break_ctl)(struct uart_port *, int ctl);
int (*startup)(struct uart_port *);
void (*shutdown)(struct uart_port *);
void (*flush_buffer)(struct uart_port *);
void (*set_termios)(struct uart_port *, struct ktermios *new,
struct ktermios *old);
void (*set_ldisc)(struct uart_port *, struct ktermios *);
void (*pm)(struct uart_port *, unsigned int state,
unsigned int oldstate);
/*
* Return a string describing the type of the port
*/
const char *(*type)(struct uart_port *);
/*
* Release IO and memory resources used by the port.
* This includes iounmap if necessary.
*/
void (*release_port)(struct uart_port *);
/*
* Request IO and memory resources used by the port.
* This includes iomapping the port if necessary.
*/
int (*request_port)(struct uart_port *);
void (*config_port)(struct uart_port *, int);
int (*verify_port)(struct uart_port *, struct serial_struct *);
int (*ioctl)(struct uart_port *, unsigned int, unsigned long);
#ifdef CONFIG_CONSOLE_POLL
int (*poll_init)(struct uart_port *);
void (*poll_put_char)(struct uart_port *, unsigned char);
int (*poll_get_char)(struct uart_port *);
#endif
};
UART 驱动编写人员需要实现 uart_ops,因为 uart_ops 是最底层的 UART 驱动接口,是实实在在的和 UART 寄存器打交道的。关于 uart_ops 结构体中的这些函数的具体含义请参考Documentation/serial/driver 这个文档。
驱动编写
以uart3为例,来讲解开发过程
1.从设备树出发,剖析开发驱动
c
uart3: serial@021ec000 {
compatible = "fsl,imx6ul-uart",
"fsl,imx6q-uart", "fsl,imx21-uart";
reg = <0x021ec000 0x4000>;
interrupts = <GIC_SPI 28 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_UART3_IPG>,
<&clks IMX6UL_CLK_UART3_SERIAL>;
clock-names = "ipg", "per";
dmas = <&sdma 29 4 0>, <&sdma 30 4 0>;
dma-names = "rx", "tx";
status = "disabled";
};
找到具体的驱动文件drivers/tty/serial/imx.c
可以看到uart3的驱动文件在tty结合起来的
c
#define SERIAL_IMX_MAJOR 207
#define MINOR_START 16
#define DEV_NAME "ttymxc"
#define MCTRL_TIMEOUT (250*HZ/1000)
#define DRIVER_NAME "IMX-uart"
#define UART_NR 8
#define IMX_RXBD_NUM 20
#define IMX_MODULE_MAX_CLK_RATE 80000000
static struct uart_driver imx_reg = {
.owner = THIS_MODULE,
.driver_name = DRIVER_NAME,
.dev_name = DEV_NAME,
.major = SERIAL_IMX_MAJOR,
.minor = MINOR_START,
.nr = ARRAY_SIZE(imx_ports),
.cons = IMX_CONSOLE,
};
...
static int __init imx_serial_init(void)
{
int ret = uart_register_driver(&imx_reg);
if (ret)
return ret;
ret = platform_driver_register(&serial_imx_driver);
if (ret != 0)
uart_unregister_driver(&imx_reg);
return ret;
}
static void __exit imx_serial_exit(void)
{
platform_driver_unregister(&serial_imx_driver);
uart_unregister_driver(&imx_reg);
}
module_init(imx_serial_init);
module_exit(imx_serial_exit);
MODULE_AUTHOR("Sascha Hauer");
MODULE_DESCRIPTION("IMX generic serial port driver");
MODULE_LICENSE("GPL");
MODULE_ALIAS("platform:imx-uart");
NXP官方驱动入口imx_serial_init
-> uart_register_driver 先向内核注册uart_driver,为imx_reg
-> platform_driver_register注册platform驱动,匹配后运行platform_driver->serial_imx_probe
-> serial_imx_probe,在这里面定义了NXP根据自己的芯片封装了uart_port的struct imx_port *sport;
serial_imx_probe主要工作是1.申请imx_prot内存,2.获取设备树中的节点信息,3.获取uart3寄存器地址,4.初始化uart_port,(主要初始化了ops)5.中断处理,串口接收中断处理函数imx_rxint获取到串口接收到的数据,然后使用tty_inert_filp_char将其放到tty中
修改设备树使能UART3
c
&uart3 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart3>;
status = "okay";
};
pinctrl_uart3: uart3grp {
fsl,pins = <
MX6UL_PAD_UART3_RX_DATA__UART3_DCE_RX 0x1b0b1
MX6UL_PAD_UART3_TX_DATA__UART3_DCE_TX 0x1b0b1
>;
};
编译,复制,启动
c
ls /dev/ttym*
/dev/ttymxc0 /dev/ttymxc2
可以在/dev/ttymxc2看到该uart3已经成功启动
我们不需要自己写应用程序来操作uart3
有现成的uart测试APP minicom
minicom移植
与dtb移植步骤类似
1.配置。2.编译。3.安装
c
./configure --prefix=/home/alientek/linux/tool/ncurses --host=arm-linux-gnueabihf --target=arm-linux-gnueabihf --with-shared --without-profile --disable-stripping --without-progs --with-manpages --without-tests
make
make install
//就可以在--prefix目录下看到安装的内容了
//将安装内容中的库文件放到开发板上去
sudo cp * ~/Desktop/nfs/rootfs/usr/lib/ -rfa
c
./configure CC=arm-linux-gnueabihf-gcc --prefix=/home/alientek/linux/tool/minicom --host=arm-linux-gnueabihf CPPFLAGS=-I/home/alientek/linux/tool/ncurses/include LDFLAGS=-L/home/alientek/linux/tool/ncurses/lib -enable-cfg-dir=/etc/minicom
make
make install
sudo cp /home/alientek/linux/tool/minicom/bin/* 开发板的/usr/bin
c
minicom -v
minicom version 2.7.1 (compiled Sep 6 2025)
Copyright (C) Miquel van Smoorenburg.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version
2 of the License, or (at your option) any later version.
c
vi /etc/passwd
#内容如下
root:x:0:0:root:/root:/bin/sh
//保存退出
export TERMINFO=/usr/share/terminfo
echo $TERMINFO
成功移植minicom
c
┌─────[configuration]──────┐
│ Filenames and paths │
│ File transfer protocols │
│ Serial port setup │
│ Modem and dialing │
│ Screen and keyboard │
│ Save setup as dfl │
│ Save setup as.. │
│ Exit │
│ Exit from Minicom │
└──────────────────────────┘