串口是很常用的一个外设,在 Linux 下通常通过串口和其他设备或传感器进行通信,根据电平的不同,串口分为 TTL 和 RS232。不管是什么样的接口电平,其驱动程序都是一样的,通过外接 RS485 这样 的芯片就可以将串口转换为 RS485 信号,正点原子的 I.MX6U-ALPHA 开发板就是这么做的。对于正点原子的 I.MX6U-ALPHA 开发板而言, RS232、 RS485 以及 GPS 模块接口通通连接到了 I.MX6U 的 UART3 接口上,因此这些外设最终都归结为 UART3 的串口驱动。
1Linux下UART驱动框架
1.1 UART驱动框架简介
同 I2C、 SPI 一样, Linux 也提供了串口驱动框架,我们只需要按照相应的串口框架编写驱动程序即可。串口驱动没有什么主机端和设备端之分,就只有一个串口驱动,而且这个驱动也已经由 NXP 官方已经编写好了,我们真正要做的就是在设备树中添加所要使用的串口节点信息。当系统启动以后串口驱动和设备匹配成功,相应的串口就会被驱动起来,生成/dev/ttymxcX(X=0….n)文件。
uart_driver 结构体表示 UART 驱动, uart_driver 定义在 include/linux/serial_core.h 文件中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 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;
};
1.2 uart_driver
注册与注销
每个串口驱动都需要定义一个
uart_driver
,加载驱动的时候通过uart_register_driver
函数向系统注册这个uart_driver
,此函数原型如下:
1 int uart_register_driver(struct uart_driver *drv)
drv: 要注册的
uart_driver
。返回值: 0,成功;负值,失败。
注销驱动的时候也需要注销掉前面注册的
uart_driver
,需要用到uart_unregister_driver
函数,函数原型如下:
1 void uart_unregister_driver(struct uart_driver *drv)
- drv: 要注销的
uart_driver
。- 返回值: 无。
1.3 uart_port
的添加与移除
uart_port
表示一个具体的 串口端口,uart_port
定义在 include/linux/serial_core.h 文件,内容如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14 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 */
............
};
- ops 包含了串口的具体驱动函数。
**每个 UART 都有一个
uart_port
**,那么uart_port
是怎么和uart_driver
结合起来的呢?这里要用到uart_add_one_port
函数,函数原型如下:
1
2 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
函数,函数原型如下:
1 int uart_remove_one_port(struct uart_driver *drv, struct uart_port *uport)
- drv:要卸载的 port 所对应的
uart_driver
。- uport: 要卸载的
uart_port
。- 返回值:0,成功;负值,失败。
1.4 uart_ops 实现
ops 包含了针对 UART 具体的驱动函数, Linux 系统收发数据最终调用的都是 ops 中的函数。 ops 是 uart_ops类型的结构体指针变量, uart_ops 定义在 include/linux/serial_core.h 文件中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46 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);
int (*poll_init)(struct uart_port *);
void (*poll_put_char)(struct uart_port *, unsigned char);
int (*poll_get_char)(struct uart_port *);
};UART 驱动编写人员需要实现
uart_ops
,因为uart_ops
是最底层的 UART 驱动接口,是实实在在的和 UART 寄存器打交道的。关于 uart_ops 结构体中的这些函数的具体含义请参考Documentation/serial/driver 这个文档
2 NXP官方驱动分析
打开 imx6ull.dtsi 文件,找到 UART3 对应的子节点,子节点内容如下所示:
1
2
3
4
5
6
7
8
9
10
11
12 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";
};查找
compatible
属性,找到imx.c里面:
1
2
3
4
5
6 static const struct of_device_id imx_uart_dt_ids[] = {
{ .compatible = "fsl,imx6q-uart", .data = &imx_uart_devdata[IMX6Q_UART], },
{ .compatible = "fsl,imx1-uart", .data = &imx_uart_devdata[IMX1_UART], },
{ .compatible = "fsl,imx21-uart", .data = &imx_uart_devdata[IMX21_UART], },
{ /* sentinel */ }
};NXP官方串口驱动入口函数为:
imx_serial_init
,此函数调用uart_register_driver
先内核注册uart_driver
,为imx_reg
,因为#define DEV_NAME "ttymxc"
,所以IMX串口为/dev/ttymxc0,1。接下来就是
uart_port
处理,NXP自定义了一个imx_port
,里面包含uart_port
。
uart_ops
为sport->port.ops = &imx_pops;
串口接收中断处理函数:
imx_rxint
获取到串口接收到的数据,然后使用tty_insert_flip_char
将其放到tty里面。
3 设备树修改
添加节点:
1
2
3
4
5 &uart3 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart3>;
status = "okay";
};
1
2
3
4
5
6 pinctrl_uart3: uart3grp {
fsl,pins = <
MX6UL_PAD_UART3_TX_DATA__UART3_DCE_TX 0x1b0b1
MX6UL_PAD_UART3_RX_DATA__UART3_DCE_RX 0x1b0b1
>;
};
4 移植minicom
minicom
类似我们常用的串口调试助手,是 Linux 下很常用的一个串口工具,将minicom
移植到我们的开发板中,这样我们就可以借助minicom
对串口进行读写操作。
4.1移植 ncurses
minicom 需要用到 ncurses, 因此需要先移植 ncurses,如果前面已经移植好了 ncurses,那么这里就不需要再次移植了,只需要在编译 minicom 的时候指定 ncurses 库和头文件目录 即可。
首先在ubuntu中创建一个目录来存放我们要移植的文件。
下载 ncurses 源码。
ncurses-6.0.tar.gz 拷贝到 Ubuntu 中创建的 tool 目录下,然后进行解压,解压命令如下:
1 tar -vxzf ncurses-6.0.tar.gz解压完成以后就会生成一个名为“ncurses-6.0”的文件夹,此文件夹就是 ncurese 的源码文件夹。在 tool 目录下新建名为“ncurses”目录,用于保存 ncurses 编译结果,一切准备就绪以后就可以编译 ncureses 库了。进入到 ncureses 源码目录下,也就是刚刚解压出来的 ncurses-6.0 目录中,首先是配置 ncureses,输入如下命令:
1 ./configure --prefix=/home/zwl/linux/tool/ncurses --host=arm-linux-gnueabihf --target=arm-linux-gnueabihf --with-shared --without-profile --disable-stripping --without-progs --with-manpages --without-tests
- configure 就是配置脚本。
- –prefix 用于指定编译结果的保存目录,这里肯定将编译结果保存到我们前面创建的“ ncurses”目录中。
- –host 用于指定编译器前缀,这里设置为 “arm-linuxgnueabihf”。
- –target 用于指定目标,这里也设置为“arm-linux-gnueabihf”。
配置成功以后输入“make”命令开始编译 。
编译成功以后输入“make install”命令安装,安装的意思就是将编译出来的结果拷贝到–pfefix 指定的目录里面去。
安装成功以后查看一下前面创建的“ncurses”文件夹,会发现里面多了一些东西。
include、 lib 和 share 这三个目录中存放的文件分别拷贝到开发板根文件系统中的/usr/include、 /usr/lib 和/usr/share 这三个目录中,如果哪个目录不存在的话请自行创建!!拷贝命令如下:
1
2
3 sudo cp lib/* /home/zuozhongkai/linux/nfs/rootfs/usr/lib/ -rfa
sudo cp share/* /home/zuozhongkai/linux/nfs/rootfs/usr/share/ -rfa
sudo cp include/* /home/zuozhongkai/linux/nfs/rootfs/usr/include/ -rfa然后在开发板根目录的/etc/profile(没有的话自己创建一个)文件中添加如下所示内容:
1
2
3
4
5
6 !/bin/sh
LD_LIBRARY_PATH=/lib:/usr/lib:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH
export TERM=vt100
export TERMINFO=/usr/share/terminfo
4.2 移植minicom
将 minicom-2.7.1.tar.gz 拷贝到 ubuntu 中的/home/zuozhongkai/linux/IMX6ULL/tool 目录下,然后在 tool 目录下新建一个名为“minicom”的子目录,用于存放 minicom 编译结果。一切准备好以后就可以编译 minicom 了,先解压 minicom,命令如下:
1 tar -vxzf minicom-2.7.1.tar.gz解压完成以后会生成一个叫做 minicom-2.7.1 的文件夹,这个就是 minicom 的源码,进入到此目录中,然后配置 minicom,配置命令如下:
1 ./configure CC=arm-linux-gnueabihf-gcc -- prefix=/home/zwl/linux/IMX6ULL/tool/minicom --host=arm-linux-gnueabihf CPPFLAGS=-I/home/zwl/linux/IMX6ULL/tool/ncurses/include LDFLAGS=-L/home/zuozhongkai/linux/IMX6ULL/tool/ncurses/lib -enable-cfg-dir=/etc/minicom
- CC 表示要使用的 gcc 交叉编译器。
- –prefix 指定编译出来的文件存放目录,肯定要存放到我们前面创建的 minicom 目录中。
- –host 指定交叉编译器前缀。
- CPPFLAGS 指定 ncurses 的头文件路径。
- LDFLAGS 指定 ncurses 的库路径。
配置成功以后执行如下命令编译并安装:
1
2 make
make install将 minicom 目录中 bin 子目录下的所有文件拷贝到开发板根目录中的/usr/bin 目录下,命令如下:
1 sudo cp bin/* /home/zwl/linux/nfs/rootfs/usr/bin/完成以后在开发板中输入“minicom -v”来查看 minicom 工作是否正常 。
从图可以看出,此时 minicom 版本号为 2.7.1, minicom 版本号查看正常。输入如下命令打开 minicom 配置界面:
1 minicom -s打开失败,新建/etc/passwd 文件,然后在 passwd 文件里面输入如下所示内容:
1 root:x:0:0:root:/root:/bin/sh开发板重启以后再执行“minicom -s”命令,此时 minicom 配置界面就可以打开了。
如果能出现图上所示界面,那么就说明 mincom 工作正常了。
5 驱动测试
5.1minicom配置
打开 minicom 配置界面,然后选中“Serial port setup” 。
选中“Serial port setup”以后点击回车,进入设置菜单
- 图中有 7 个设置项目,分别对应 A、 B……G,比如第一个是选中串口, UART3 的串口文件为/dev/ttymxc2,因此串口设备要设置/dev/ttymxc2。设置方法就是按下键盘上的‘A’,然后输入“/dev/ttymxc2”即可,都设置完成以后按下回车键确认并退出。
- 设置完以后按下回车键确认,确认完以后就可以设置其他的配置项。比如 E 设置波特率、数据位和停止位的、 F 设置硬件流控的,设置方法都一样。
按下ESC 键退出,退出后显示下图所示串口调试界面,可以看出当前的串口文件为/dev/ttymxc2,按下 CTRLA,然后再按下 Z 就可以打开 minicom 帮助信息界面。
可以看出, minicom 有很多快捷键,本实验我们打开 minicom 的回显功能,回显功能配置项为“local Echo on/off..E”,因此按下 E 即可打开/关闭回显功能。
5.2RS232测试
mini板没有RS232。放弃。
5.3RS485测试
自己没有测试模块。遂放弃。
5.4GPS测试
GPS买了没啥用,还挺贵,就为了做个实验不值当。还是放弃。
5.5串口测试
USB转TTL还是有的。可以做测试。
开发板连接:TX->RX,RX->TX。
直接输入,会看到串口工具会显示。并且收发都能成功。