串口是很常用的一个外设,在Linux下通常通过串口和其他设备或传感器进行通信,根据
电平的不同,串口分为TTL和RS232。不管是什么样的接口电平,其驱动程序都是一样的,通过外接RS485这样的芯片就可以将串口转换为RS485信号,正点原子的STM32MP1开发板就是这么做的。对于正点原子的STM32MP1开发板而言有8个串口,四个同步串口(USART1、USART2、USART3和USART6),四个异步串口(UART4、UART5、UART7和UART8)。
RS232和RS485接口连接到了STM32MP1的USART3接口上,通过跳线帽选择USART3作为RS232还是RS485。GPS模块是连接到UART5接口上,因此这些外设最终都归结为USART3和UART5的串口驱动。本章就来学习一下如何驱动STM32MP1开发板上的USART3串口和UART5,进而实现RS232、RS485以及GPS驱动。
同I2C、SPI一样,Linux也提供了串口驱动框架,只需要按照相应的串口框架编写驱动程序即可。串口驱动没有什么主机端和设备端之分,就只有一个串口驱动,而且这个驱动也
已经由ST官方编写好了,真正要做的就是在设备树中添加所要使用的串口节点信息。当
系统启动以后串口驱动和设备匹配成功,相应的串口就会被驱动起来,生成/dev/ttySTMX(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 *uart)
函数参数和返回值含义如下:
注销驱动的时候也需要注销掉前面注册的uart_driver,需要用到uart_unregister_driver函数,函数原型如下:
void uart_unregister_driver(struct uart_driver *uart)
函数参数和返回值含义如下:
uart_port表示一个具体的port,uart_port定义在 include/linux/serial_core.h文件,内容如下(有省略):
uart_port中最主要的就是第240行的ops,ops包含了串口的具体驱动函数。每个UART都有一个uart_port,那么uart_port是怎么和uart_driver结合起来的呢?这里要用到uart_add_one_port函数,函数原型如下:
int uart_add_one_port(struct uart_driver *reg, struct uart_port *port)
函数参数和返回值含义如下:
卸载UART驱动的时候也需要将uart_port从相应的uart_driver中移除,需要用到uart_remove_one_port函数,函数原型如下:
int uart_remove_one_port(struct uart_driver *reg, struct uart_port *port)
函数参数和返回值含义如下:
在上面讲解uart_port的时候说过,uart_port中的ops成员变量很重要,因为ops包含了针对UART具体的驱动函数,Linux系统收发数据最终调用的都是ops中的函数 。ops是uart_ops类型的结构体指针变量,uart_ops定义在include/linux/serial_core.h文件中,内容如下:
示例代码 46.1.3 uart_port 结构体
37 struct uart_ops {
38 unsigned int (*tx_empty)(struct uart_port *);
39 void (*set_mctrl)(struct uart_port *, unsigned int mctrl);
40 unsigned int (*get_mctrl)(struct uart_port *);
41 void (*stop_tx)(struct uart_port *);
42 void (*start_tx)(struct uart_port *);
43 void (*throttle)(struct uart_port *);
44 void (8unthrottle)(struct uart_port *);
45 void (*send_xchar)(struct uart_port *, char ch);
46 void (*stop_rx)(struct uart_port *);
47 void (*enable_ms)(struct uart_port *);
48 void (*break_ctl)(struct uart_port *, int ctl);
49 int (*startup)(struct uart_port *);
50 void (*shutdown)(struct uart_port *);
51 void (*flush_buffer)(struct uart_port *);
52 void (*set_termios)(struct uart_port *, struct ktermios *new,
53 struct ktermios *old);
54 void (*set_ldisc)(struct uart_port *, struct ktermios *);
55 void (*pm)(struct uart_port *, unsigned int state,
56 unsigned int oldstate);
57
58 /*
59 * Return a string describing the type of the port
60 */
61 const char *(*type)(struct uart_port *);
62
63 /*
64 * Release IO and memory resources used by the
65 * This includes iounmap if
66 */
67 void (*release_port)(struct uart_port *);
68
69 /*
70 * Request IO and memory resources used by the
71 * This includes iomapping the port if
72 */
73 int (*request_port)(struct uart_port *);
74 void (*config_port)(struct uart_port *, int);
75 int (*verify_port)(struct uart_port *, struct serial_struct );
76 int (*ioctl)(struct uart_port *, unsigned int, unsigned long);
77 #ifdef CONFIG_CONSOLE_POLL
78 int (*poll_init)(struct uart_port *);
79 void (*poll_put_char)(struct uart_port *, unsigned char);
80 int (*poll_get_char)(struct uart_port *);
81 #endif
82 };
UART驱动编写人员需要实现uart_ops,因为uart_ops是最底层的UART驱动接口,是实实在在的和UART寄存器打交道的。关于uart_ops结构体中的这些函数的具体含义请参考Documentation/serial/driver这个文档。
UART驱动框架大概就是这些,接下来理论联系实际,看一下ST官方的UART驱动文件是如何编写的。
打开stm32mp151.dtsi文件,找到USART3对应的子节点,子节点内容如下所示:
重点看一下第2行的compatible属性值为“st,stm32h7-uart”。在linux源码中搜索这个值即可找到对应的UART驱动文件,此文件为drivers/tty/serial/stm32-usart.c,在此文件中可以找到如下内容:
示例代码 46.2.2 UART platform 驱动框架
1218 static const struct of_device_id stm32_match[] = {
1219 { .compatible = "st,stm32 uart", .data = &stm32f4_info},
1220 { .compatible = "st,stm32f7 uart", .data = &stm32f7_info},
1221 { .compatible = "st,stm32h7 uart", .data = &stm32h7_info},
1222 {},
1223 };
......
1668 static struct platform_driver stm32_serial_driver = {
1669 .probe = stm32_usart_serial_probe,
1670 .remove = stm32_usart_serial_remove,
1671 .driver = {
1672 .name = DRIVER_NAME,
1673 .pm = &stm32_serial_pm_ops,
1674 .of_match_table = of_match_ptr(stm32_match),
1675 },
1676 };
1677
1678 static int __init stm32_usart_init(void)
1679 {
1680 static char banner[] __initdata = "STM32 USART driver initialized";
1681 int ret;
1682
1683 pr_info("%s\n", banner);
1684
1685 ret = uart_register_driver(&stm32_usart_driver);
1686 if (ret)
1687 return ret;
1688
1689 ret = platform_driver_register(&stm32_serial_driver);
1690 if (ret)
1691 uart_unregister_driver(&stm32_usart_driver);
1692
1693 return ret;
1694 }
1695
1696 static void __exit stm32_usart_exit(void)
1697 {
1698 platform_driver_unregister(&stm32_serial_driver);
1699 uart_unregister_driver(&stm32_usart_driver);
1700 }
1701
1702 module_init(stm32_usart_init);
1703 module_exit(stm32_usart_exit);
可以看出STM32MP1的UART本质上是一个platform驱动,第1218-1223行,设备树所
使用的匹配表,第1221行的compatible属性值为“st,stm32h7-uart”。
第1668-1676行,platform驱动框架结构体stm32_serial_driver。
第1678-1694行,驱动入口函数,第1685行调用uart_register_driver函数向Linux内核注册uart_driver,在这里就是stm32_usart_driver。
第1696-1700行,驱动出口函数,第1699行调用uart_unregister_driver函数注销掉前面注册的uart_driver,也就是stm32_usart_driver。
在stm32_usart_init函数中向Linux内核注册了stm32_usart_driver,stm32_usart_driver就是uart_driver类型的结构体变量,stm32_usart_driver定义如下:
当UART设备和驱动匹配成功以后stm32_usart_serial_probe函数就会执行,此函数的重点工作就是初始化uart_port,然后将其添加到对应的uart_driver中。在看stm32_usart_serial_probe函数之前先来看一下stm32_port结构体,stm32_port是ST为STM32MP1系列SOC定义的一个设备结构体,此结构体内部就包含了uart_port成员变量,stm32_port结构体内容如下所示(有
缩减):
第258行,uart_port结构体成员变量:port。
第279行,这里定义了一个数组为stm32_ports,数组的类型为stm32_port结构体,数组的长度为8。这是因为STM32MP157最多只有8个串口,一个串口对应一个stm32_port,因此数组长度就是8。
接下来看一下stm32_usart_serial_probe函数,函数内容如下:
第1312行,调用stm32_usart_of_get_port函数,它主要是负责配置stm32_ports数组。
第1322行,调用stm32_usart_init_port函数,它主要是负责获取SOC UART外设首地址、
中断号、注册中断函数同时还设置uart_ops为stm32_uart_ops,stm32_uart_ops就是STM32MP1最底层的驱动函数集合。
第1374行,使用uart_add_one_port向uart_driver添加uart_port,在这个就是向stm32_usart_driver添加stm32port->port。
接下来看一下stm32_usart_of_get_port函数,因为stm32_usart_serial_probe函数会调用此函数来获取串口信息,这些串口信息会放到示例代码46.2.4中的stm32_ports数组里面。stm32_usart_of_get_port函数源码如下:
第1197行,通过of_alias_get_id函数从设备树的aliases节点中获取“serial”相关的ID。打开stm32mp157d-atk.dts文件,当前此文件里面的aliases节点内容如下图所示:
从上图可以看出,此时alases节点里面只有一个serial0,对应STM32MP157的uart4。所以stm32_usart_of_get_port函数只能得到uart4这一个串口的信息,如果要使用其他的串口,那就必须向alases节点里面按照如下格式添加对应的串口信息:
serialX=&串口名字; |
这个X表示0-7,那是因为STM32MP1的串口只有8个。&后面的串口名字一定要对应设备树中具体的串口名,比如usart3、uart5等。
第1206-1213行,获取对应的串口信息,然后保存到stm32_ports数组中,获取到的串口ID就是串口在数组中的索引。
第1214行,返回得到的串口信息。
接下来再来看一下stm32_usart_init_port函数,stm32_usart_serial_probe函数会调用此函数来初始化串口!函数源码如下所示:
stm32_usart_init_port函数主要是负责获取SOC UART外设首地址、中断号、注册中断函数。重点是1149行设置uart_ops为stm32_uart_ops,stm32_uart_ops就是 STM32MP1最底层的驱动函数集合。
stm32_uart_ops就是uart_ops类型的结构体变量,保存了STM32MP1串口最底层的操作函
数,stm32_uart_ops定义如下:
stm32_uart_ops中的函数基本都是和STM32MP1的UART寄存器打交道的,这里就不去详细的分析了。简单的了解了STM32MP1的UART驱动以后再来学习一下,如何驱动正点原子STM32MP1开发板上的USART3接口和UART5接口。
本实验要用到的STM32MP1的USART3接口和UART5接口,USART3连接RS485和RS232的公头,UART5连接GPS和RS232的母头。依次来看一下这个两个串口的硬件原理图。
RS232原理图如下图所示:
正点原子STM32MP157开发板一共有2个RS232串口,上图中COM1是母头,COM2为公头,这两个RS232串口都是通过SP3232这个芯片来实现。
COM1母头连接到STM32MP1的UART5接口上,COM1和正点原子的ATK模块共用USART5,把JP5的1-3和2-4连接起来以后SP3232就和URAT5连接到一起了。 UART5_TX和UART5_RX分别接到了PB13和PB12这两个引脚上。
COM2公头连接到了STM32MP1的USART3接口上,COM2和RS485共用USART3,把JP4的3-5和4-6连接起来以后SP3232就和USRAT3连接到一起了。USART3_TX和USART3_RX分别接到了PD8和PD9这两个引脚上。
RS485和COM2共用USART3,将上图中JP4的3-5和4-6连接起来,这时候RS485就连接到了USART3上。RS485原理图如下图所示:
RS485采用SP3485这颗芯片来实现,RO为数据输出端,RI为数据输入端,RE是接收使能信号(低电平有效),DE是发送使能信号(高电平有效)。在上图中RE和DE经过一系列的电路,最终通过RS485_RX来控制,这样可以省掉一个RS485收发控制IO,将RS485完全当作一个串口来使用,方便写驱动。
正点原子有一款GPS+北斗定位模块,型号为ATK1218-BD,STM32MP157开发板留出了这款GPS定位模块的接口,接口原理图如下图所示:
前面讲解RS232原理图的时候说了,COM1和正点原子的ATK模块共用USART5接口,正点原子的ATK1218-BD这个模块用的就是ATK模块接口。如果要使用GPS模块,就要将RS232原理图中JP5的3-5和4-6连接起来。这样GPS模块就连接到了USART5上,USART5驱动成功以后就可以直接读取GPS模块数据了。从上图可以看出,ATK模块还有两个引脚GBC_KEY和GBC_LED分别连接到了STM32MP157的PC13和PI8上,这两个引脚是给其他模块准备的,GPS模块并没有用到。
前面已经说过了,STM32MP1的UART驱动ST已经编写好了,所以不需要自行编写。要做的就是在设备树中添加USART3和UART5对应的设备节点即可。打开stm32mp157d-atk.dts文件,因为usart3和uasrt5的节点在stm32mp151.dtsi已经存在了,只要在stm32mp157d-atk.dts文件里面向这些节点追加一些内容即可,追加步骤如下:
先在stm32mp15-pinctrl.dtsi文件看下没有usart3和uart5的引脚配置,以及引脚配置是
否是开发板对应的。默认情况下stm32mp15-pinctrl.dtsi里面是有usart3的引脚配置,但是
不是正点原子开发板所使用的PD8和PD9,所以不能使用。直接在stm32mp15-pinctrl.dtsi里面添加usart3和uart5这两个串口对应的引脚信息,内容如下:
示例代码 46.4.1 要追加的 pinmux 配置
1 usart3_pins_c: usart3-2 {
2 pins1 {
3 pinmux = <STM32_PINMUX('D', 8, AF7)>; /* USART3_TX */
4 bias-disable;
5 drive-push-pull;
6 slew-rate = <0>;
7 };
8 pins2 {
9 pinmux = <STM32_PINMUX('D', 9, AF7)>; /* USARTS_RX */
10 bias-disable;
11 };
12 };
13
14 uart5_pins_a: uart5-0 {
15 pins1 {
16 pinmux = <STM32_PINMUX('B', 13, AF14)>; /* UART5_TX */
17 bias-disable;
18 drive-push-pull;
19 slew-rate = <0>;
20 };
21 pins2 {
22 pinmux = <STM32_PINMUX('B', 12, AF14)>; /* UART5_RX */
23 bias-disable;
24 };
25 };
示例代码46.4.1里配置了两个pinmux分别为usart3_pins_c和uart5_pins_a。稍后向usart3和uart5中追加内容的时候就会用到这两个节点。
还是在stm32mp157d-atk.dts文件中,在不是根节点下追加如下代码:
示例代码 46.4.2 串口的节点
1 &usart3 {
2 pinctrl-names = "default";
3 pinctrl-0 = <&usart3_pins_c>;
4 status = "okay";
5 };
6
7 &uart5 {
8 pinctrl-names = "default";
9 pinctrl-0 = <&uart5_pins_a>;
10 status = "okay";
11 };
这里追加了两个串口,分别为uart5和usart3,追加的内容很简单都是使用了刚刚添加的pinmux配置。把status属性原来为“disabled”改为“okay”。
之前UART驱动分析已经知道,驱动会读取aliases节点,添加的别名如下所示:
示例代码 46.4.3 串口的别名
1 aliases {
2 serial0 = &uart4;
3 serial1 = &uart5;
4 serial2 = &usart3;
5 };
serial0是uart4的别名,表示在系统启动生成一个名为“/dev/ttySTM0”的设备文件serial1就会生成“/dev/ttySTM1”如此类推,最多8个。serial0就是调试串口。
修改完成以后重新编译设备树并使用新的设备树启动Linux,如果设备树修改成功的话系统启动以后就会有如下图所示设备文件:
ttySTM0为serial0,对应uart4;ttySTM1为serial1,对应uart5;ttySTM2为serial2,对应usart3。
minicom类似常用的串口调试助手,是Linux下很常用的一个串口工具,将minicom移植到开发板中,这样就可以借助minicom对串口进行读写操作。
buildroot已经集成了minicom,所以只需要重新配置buildroot,使能minicom即可。首先跳转到buildroot的源码目录下,打开buildroot的图形化配置界面里配置以下选项:
-> Target packages -> Hardware handling [*] minicom |
配置如下图所示:
保存buildroot的配置文件,输入命令“sudo make”重新编译文件系统。编译的时候要联网,因为buildroot在编译的时候需要从网上下载 minicom源码。当编译完成后,进入output/images目录,运行以下命令把文件系统替换进去:
cd output/images/ //进入到 output/images目录 sudo tar -axvf rootfs.tar -C /home/zuozhongkai/linux/nfs/rootfs //解压到 nfsroot目录 |
上述命令将buildroot中output/images/rootfs.tar这个压缩包解压到/home/zuozhongkai/linux/nfs/rootfs这个目录中,这个目录就是当前nfsroot目录,需要根据自己的实际情况解压到对应的目录文件中。
完成以后重启开发板!重启以后在开发板中输入“minicom -v”来查看minicom工作是否正常,结果如下图所示:
从上图可以看出,此时minicom版本号为2.7.90,minicom版本号查看正常。输入如下命令打开minicom配置界面:
minicom -s |
此时minicom配置界面就可以打开了,如下图所示:
如果出现如上图所示的界面,那么minicom就已经能够正常工作了。
在测试之前要先将STM32MP1开发板的RS232接口与电脑连接起来,正点原子STM32MP1开发板上两个RS232接口如下图所示:
从上图中可以看出,正点原子开发板上有2个 RS232接口。这里要注意的是这两个RS232接口一个为公头,一个为母头,方便外接自己的设备。上图中左边的COM2为公头,可以通过JP4跳接到USART3上。右边的COM1为母头,可以通过JP5跳接到UART5上。本实验使用右边的COM1,所以需要将JP5的两个跳线帽接到上方,也就是将UART5与COM1连接起来。
跳线帽设置好以后使用RS232线将开发板与电脑连接起来,这里建议使用USB转DB9(RS232)数据线,比如正点原子的CH340方案的USB转DB9数据线,如下图所示:
上图中所示的数据线是带有CH340芯片的,因此当连接到电脑以后就会出现一个COM口,这个COM口就是要使用的COM口。比如在正点原子教程中的电脑上就是COM11,在MobaXterm上新建一个连接,串口为COM11,波特率为115200。
在开发板中输入“minicom -s”,打开minicom配置界面,选中“Serial port setup”,如下图所示:
选中“Serial port setup”点击回车,进入设置菜单,如下图所示:
上图中有14个设置项目,分别对应A、B……N,比如第一个是选中串口UART5的串口文件为/dev/ttySTM1(因为设备别名serial1=&UART5),因此串口设置要设置为/dev/ttySTM1。设置方法就是按下键盘上的‘A’,然后输入“/dev/ttySTM1”即可,如下图所示:
设置完以后按下回车键确认,确认完以后就可以设置其他的配置项。比如‘E’设置波特率、数据位和停止位的、‘F’设置硬件流控的,设置方法都一样,设置完以后如下图所示:
都设置完成以后按下回车键确认并退出,这时候会退回到之前的界面,按下ESC键退出配置界面,退出以后如下图所示:
上图就是串口调试界面,可以看出当前的串口文件为/dev/ttySTM1,按下CTRL-A,然后再按下Z就可以打开minicom帮助信息界面,如下图所示:
从上图可以看出,minicom有很多快捷键,本实验打开minicom的回显功能,回显功能配置项为“local Echo on/off…E”,因此按下E即可打开/关闭回显功能。
首先测试开发板通过UART5向电脑发送数据的功能,需要打开minicom的回显功能(不打开也可以,但是在minicom中看不到自己输入的内容),回显功能打开以后输入“AAAA”,如下图所示:
上图中的“AAAA”就是开发板通过UART5向电脑发送的数据,那么电脑的COM11就会接收到“AAAA”,MobaXterm中COM11收到的数据如下图所示:
可以看出,开发板通过UART3向电脑发送数据正常,接下来测试开发板数据接收功能。
接下来测试开发板的UART5接收功能,同样的,要先打开MobaXterm上COM11的本地回显,正点原子教程里面没有指导该功能,但是开发板是可以接收到在COM11上输入的字符。比如,这里输入‘123456’,此时开发板接收到的数据如下图所示:
UART5收发测试都没有问题,说明UART5驱动工作正常。如果要退出minicom,在minicom通信界面按下CRTL+A,然后按下X来关闭minicom。
前面已经说过了,STM32MP1开发板上的RS485接口连接到了USART3上,因此本质上就是个串口。 RS232实验已经将USART3的驱动编写好了,所以RS485实验就不需要编写任何驱动程序,可以直接使用minicom来进行测试。
首先是设置JP4跳线帽,将1-3、2-4连接起来, RS485接口如下图所示:
一个板子是不能进行RS485通信测试的,还需要另一个RS485设备,比如另外一块STM32MP1开发板。这里可以使用正点原子出品的USB三合一串口转换器,支持USB转TTL、RS232和RS485,如下图所示:
使用杜邦线将USB串口转换器的RS485接口和STM32MP157开发板的RS485连接起来,A接A,B接B,不能接错了!连接完成以后如下图所示:
串口转换器通过USB线连接到电脑上,用的是 CH340版本的,因此就不需要安装驱动,如果使用的是FT232版本的就需要安装相应的驱动。连接成功以后电脑就会有相应的COM口,比如教程中电脑上就是COM6,接下来就是测试。
RS485的测试和RS232一模一样!电脑上USB多合一转换器对应COM12。因为MobaXterm没有找到回显设置,因此这里为了方便观察,USB多合一转换器使用SecureCRT这个终端软件。使用SecureCRT创建一个COM12的连接,开发板使用USART3,对应的串口设备文件为/dev/ttySTM2,因此开发板使用minicom创建一个/dev/ttySTM2的串口连接。串口波特率都选择115200 8位数据位,1位停止位,关闭硬件和软件流控。
首先测试开发板通过RS485发送数据,设置好minicom以后,同样输入“AAAA”,也就是通过RS485向电脑发送一串“AAAA”。如果RS485驱动工作正常的话,那么电脑就会介绍到开发板发送过来的“AAAA”,如下图所示:
从上图可以看出开发板通过RS485向电脑发送“AAAA”成功,说明RS485数据发送正常。
接下来测试一下RS485数据接收,电脑通过RS485向开发板发送“BBBB”,然后观察minicom是否能接收到“BBBB”。结果如下图所示:
从上图中可以看出开发板接收到电脑通过RS485发送过来的“BBBB”,说明RS485数据接收正常。
GPS模块大多数都是串口输出的,这里以正点原子出品的ATK118-BD模块为例,这是一款GSP+北斗的定位模块,如下图所示:
首先要设置STM32MP1开发板上的JP5跳线帽,将UART5与ATK模块接口上的串口连接起来,如下图所示:
此时UART5_TX和UART5_RX已经连接到了开发板上的ATK MODULE上,直接将ATK1218-BD模块插到开发板上的ATK MODULE接口即可,开发板上的ATK MODULE接口是6脚的,而 ATK1218-BD模块是5脚的,因此需要靠下插(VCC对应 5V)!然后GPS需要接上天线,天线的接收头一定要放到户外,因此室内一般是没有GPS信号的。连接完成以后如下图所示:
GPS都是被动接收定位数据的,模块接收定位卫星数据,然后计算出位置信息通过串口输出。所以要先设置minicom,UART5对应/dev/ttySTM1,串口设置要求如下:
设置好以后如下图所示:
设置好以后就可以静静的等待GPS数据输出,GPS模块第一次启动可能需要几分钟搜星,
等搜到卫星以后才会有定位数据输出。搜到卫星以后GPS模块输出的定位数据如下图所示:
这一篇实验,其实就是对于STM32MP157开发板的串口的使用,这里驱动也是ST官方已经写好的,我们需要做的就是直接在pinctrl加节点写好GPIO的复用,然后在设备树里面添加对应的串口节点,并关联上pinctrl。还有就是在buildroot里面要配置一下minicom这个串口调试,方便测试串口。
之后的测试,RS485个人觉得可以关注一下,至于RS232可以看到其实现在用的已经很少了,就直接都是RS232转串口就可以了,比如精英板就是串口直接Type-C了,不会再用RS232。