11. 串口通讯¶
本章主要讲解LubanCat-RK系列板卡的40pin引脚中串口的基本使用。
本章的示例代码目录为:base_linux/uart
11.1. 串口引脚关系¶
其中串口的引脚关系如下表所示
串口 |
引脚 |
功能 |
---|---|---|
TXD |
8 |
发送信号线 |
RXD |
10 |
接受信号线 |
对应实物的40pin接口
LubanCat-Zero系列使用的是uart8
LubanCat-1和LubanCat-2系列使用的是uart3
LubanCat-4使用的是uart0
虽然名字不一样,操作方式是一样的。
LubanCat-Q1有4个Uart串口,分别是UART1、UART3、UART6、UART7
其中串口的引脚关系如下表所示
串口 |
引脚 |
功能 |
---|---|---|
UART6_RX |
3 |
接收信号线 |
UART6_TX |
5 |
发送信号线 |
UART1_TX |
8 |
发送信号线 |
UART1_RX |
10 |
接收信号线 |
UART3_RX |
22 |
接收信号线 |
UART3_TX |
26 |
发送信号线 |
UART7_RX |
27 |
接收信号线 |
UART7_TX |
28 |
发送信号线 |
对应实物的30pin接口
11.2. 使能串口接口¶
串口在默认情况是关闭状态的,需要使能才能使用。
11.2.1. 方法一¶
1 2 3 4 5 | #进入工具配置
sudo fire-config
#移动光标到下图的位置
#按确认键进入配置
|
打开串口
使用方向键移动光标到
UART
按 “空格键” 选中UART(出现 “*” ),如下图
按 “确认键” 进行设置
按 “Esc键” 退出到终端,运行 sudo reboot 进行重启应用
11.2.2. 方法二¶
板子名称 |
配置文件名称 |
说明 |
---|---|---|
当前使用的板卡 |
uEnv.txt |
系统会自动把板卡的配置文件链接到该文件 |
LubanCat-RK |
uEnvLubanCat-series.txt |
通用板卡的配置文件,用于第一次启动配置系统 |
Lubancat-Zero-N |
uEnvLubanCatZN.txt |
适用于EBF410068 EBF410068V1 |
Lubancat-Zero-W |
uEnvLubanCatZW.txt |
适用于EBF410067 EBF410067V1 |
Lubancat-1 |
uEnvLubanCat1.txt |
适用于EBF410077 EBF410077V1 EBF410077V2 |
Lubancat-1N |
uEnvLubanCat1N.txt |
适用于EBF410052 EBF410052V1 |
Lubancat-1金手指/BTB |
uEnvLubanCat1IO.txt |
适用于EBF410090 EBF410132 |
Lubancat-2 |
uEnvLubanCat2.txt |
适用于EBF410044 |
Lubancat-2V1 |
uEnvLubanCat2-V1.txt |
适用于EBF410044V1 |
Lubancat-2V2 |
uEnvLubanCat2-V2.txt |
适用于EBF410044V2 |
Lubancat-2-n |
uEnvLubanCat2N.txt |
适用于EBF410076 EBF410076V1 |
Lubancat-2金手指/BTB |
uEnvLubanCat2IO.txt |
适用于EBF410298 EBF410297 |
Lubancat-4 |
uEnvLubanCat4.txt |
适用于EBF410116 |
Lubancat-Q1 |
uEnvLubanCatQ1.txt |
适用于EBF410434 |
可以通过打开 /boot/uEnv/board.txt (board是你所用的板子的名称),一般第一次启动已经初始化将板级uEnv.txt软连接到了/boot/uEnv/uEnv.txt,可以直接修改该文件。
查看是否启用了uart相关设备设备树插件。编辑文件,将带有 uart (以uart3为例) 的那一行的注释符号去掉 如下图:
然后重启激活设备
注解
如果是直接拔电源的方式重启,会有可能出现文件没能做出修改 (原因:文件未能及时从内存同步到存储设备中,解决方法,在终端上输入 “sync” 再拔电关机)
11.3. 检查串口设备¶
查看串口有没有成功使能
1 2 | #执行命令查看终端设备
ls /dev/tty*
|
LubanCat-Zero系列使用的是ttyS8
LubanCat-1和LubanCat-2系列使用的是ttyS3
LubanCat-4使用的是ttyS0
LubanCat-Q1串口与设备对应关系如下表所示
串口 |
设备号 |
---|---|
UART1 |
ttyS1 |
UART3 |
ttyS3 |
UART6 |
ttyS6 |
UART7 |
ttyS7 |
如下图(作者使用LubanCat-1):
11.4. 串口通讯实验(Shell)¶
本次实验以LubanCat-1板卡讲解,其他板子的操作和本实验类似,这里就不过多赘述了。 使用板卡上的串口3进行实验,对应的设备文件为/dev/ttyS3。 对tty的设备文件直接读写就可以控制设备通过串口接收或发送数据,下面我们使用板卡配合Windows下的串口调试助手或Linux下的minicom进行测试。
11.4.2. 查询串口3的通信参数¶
串口3外设使能后,在/dev目录下生成ttyS3设备文件,用stty工具查询其通信参数
1 2 | #在板卡的终端执行如下命令
stty -F /dev/ttyS3
|
如下图:
11.4.3. 修改串口波特率¶
1 2 | #设置通讯速率,其中ispeed为输入速率,ospeed为输出速率
stty -F /dev/ttyS3 ispeed 115200 ospeed 115200
|
如下图:
11.4.5. 与Windows主机通讯¶
11.4.5.1. 串口通讯实验¶
配置好串口调试助手后,尝试使用如下命令测试发送数据:
1 2 3 4 5 | #在板卡上的终端执行如下指令
#使用echo命令向终端设备文件写入字符串"Hello!"、"I'm lubancat"
echo Hello! > /dev/ttyS3
echo "I'm lubancat" > /dev/ttyS3
#Windows上的串口调试助手会接收到内容
|
如下图:
可以看到,往/dev/ttyS3设备文件写入的内容会直接通过串口线发送至Winodws的主机。
而读取设备文件则可接收Winodws主机发往板卡的内容,可以使用cat命令来读取:
1 2 3 4 5 6 7 | #在板卡上的终端执行如下指令
#使用cat命令读取终端设备文件
cat /dev/ttyS3
#cat命令会等待
#使用串口调试助手发送字符串
#字符串最后必须加回车!
#板卡的终端会输出接收到的内容
|
如下图:
Hello lubancat!
11.4.5.2. minicom通讯¶
1 2 3 4 5 | #安装minicom软件包
sudo apt install minicom
#设置串口
sudo minicom -s
|
如下图:
进入设置,修改串口或者波特率,按键盘上的字母进入各自要设置的东西,enter键确认
设置完成后,可以按这按键保存配置,保存后,以后打开不用设置
然后按exit键进入minicom的终端
如果你输入字母,屏幕上没反应,可以通过打开回显来显示, 先按“ctrl + A” 再按’z’键进入菜单
按下’e’,回显就打开成功了(左下角会有提示,打开或者关闭),可以按下按键测试一下,观察是否有回显, 我们还可以先按“ctrl + A” 再按’z’键,再按’c’键来清除屏幕, 将板卡和电脑用串口线连起来,同时设置为115200, 在板卡上,输入”Hello! i’m lubancat!’’
注意
使用minicom时,不能使用退格键把发出去的内容删掉, minicom是以单个字符的方式发送的。
我们可以在pc端发送文字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | minicom的菜单有很多功能,
s ---- 发送文件
p ---- 设置通讯参数,包括一些预设的波特率,数据格式,数据位等
l ---- 就可以将log信息保存到一个文件中了,方便查看
t ---- 设置终端参数,以及键位设置
w ---- 超出一行的数据后自动换行
r ---- 接收文件
a ---- 换行发送时会增加时间戳
n ---- 增加时间戳
c ---- 清除屏幕
o ---- 设置minicom,相当于sudo minicom -s
j ---- 休眠状态
x ---- 退出的同时复位
q ---- 退出
你也可以不用通过按 "ctrl + a" + 'z' + '?'的方式设置
而是直接使用"ctrl + a" + '?'
|
11.5. 串口通讯实验(系统调用)¶
如果只是想通过串口终端设备收发数据,那么使用open、read、write等系统调用能轻易实现, 操作的原理和前面的led、gpio、input设备并无区别,都是读写设备文件。 但是led、gpio和input除了主设备文件,还有众多的属性文件配合用于设置设备的运行参数, 如led的trigger文件,gpio的direction文件,而终端设备却没有其它的属性文件, 那么stty命令和minicom工具是如何配置终端设备参数的呢?
11.5.1. 实验¶
11.5.1.1. 代码¶
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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | #include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <termios.h>
#include <string.h>
#include <sys/ioctl.h>
//第一部分代码/
//根据具体的设备修改
const char default_path[] = "/dev/ttyS3";
int main(int argc, char *argv[])
{
int fd;
int res;
char *path;
char buf[1024] = "Embedfire tty send test.\n";
//第二部分代码/
//若无输入参数则使用默认终端设备
if (argc > 1)
path = argv[1];
else
path = (char *)default_path;
//获取串口设备描述符
printf("This is tty/usart demo.\n");
fd = open(path, O_RDWR);
if (fd < 0) {
printf("Fail to Open %s device\n", path);
return 0;
}
//第三部分代码/
struct termios opt;
//清空串口接收缓冲区
tcflush(fd, TCIOFLUSH);
// 获取串口参数opt
tcgetattr(fd, &opt);
//设置串口输出波特率
cfsetospeed(&opt, B9600);
//设置串口输入波特率
cfsetispeed(&opt, B9600);
//设置数据位数
opt.c_cflag &= ~CSIZE;
opt.c_cflag |= CS8;
//校验位
opt.c_cflag &= ~PARENB;
opt.c_iflag &= ~INPCK;
//设置停止位
opt.c_cflag &= ~CSTOPB;
//更新配置
tcsetattr(fd, TCSANOW, &opt);
printf("Device %s is set to 9600bps,8N1\n",path);
//第四部分代码/
do {
//发送字符串
write(fd, buf, strlen(buf));
//接收字符串
res = read(fd, buf, 1024);
if (res >0 )
//给接收到的字符串加结束符
buf[res] = '\0';
printf("Receive res = %d bytes data: %s\n",res, buf);
} while (res >= 0);
printf("read error,res = %d",res);
close(fd);
return 0;
}
|
11.5.1.3. 运行¶
1 2 | #运行
sudo ./uart_t
|
运行程序
在串口助手上发送字符串
可以看到返回了两个
Hello LubanCat !!!
一个是回显,另一个是程序将内容发送到主机内
11.5.2. 代码分析¶
我们直接通过修改串口终端参数的示例代码来解答这个疑惑,源码如下所示。
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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | #include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <termios.h>
#include <string.h>
#include <sys/ioctl.h>
//第一部分代码/
//根据具体的设备修改
const char default_path[] = "/dev/ttyS3";
int main(int argc, char *argv[])
{
int fd;
int res;
char *path;
char buf[1024] = "Embedfire tty send test.\n";
//第二部分代码/
//若无输入参数则使用默认终端设备
if (argc > 1)
path = argv[1];
else
path = (char *)default_path;
//获取串口设备描述符
printf("This is tty/usart demo.\n");
fd = open(path, O_RDWR);
if (fd < 0) {
printf("Fail to Open %s device\n", path);
return 0;
}
//第三部分代码/
struct termios opt;
//清空串口接收缓冲区
tcflush(fd, TCIOFLUSH);
// 获取串口参数opt
tcgetattr(fd, &opt);
//设置串口输出波特率
cfsetospeed(&opt, B9600);
//设置串口输入波特率
cfsetispeed(&opt, B9600);
//设置数据位数
opt.c_cflag &= ~CSIZE;
opt.c_cflag |= CS8;
//校验位
opt.c_cflag &= ~PARENB;
opt.c_iflag &= ~INPCK;
//设置停止位
opt.c_cflag &= ~CSTOPB;
//更新配置
tcsetattr(fd, TCSANOW, &opt);
printf("Device %s is set to 9600bps,8N1\n",path);
//第四部分代码/
do {
//发送字符串
write(fd, buf, strlen(buf));
//接收字符串
res = read(fd, buf, 1024);
if (res >0 )
//给接收到的字符串加结束符
buf[res] = '\0';
printf("Receive res = %d bytes data: %s\n",res, buf);
} while (res >= 0);
printf("read error,res = %d",res);
close(fd);
return 0;
}
|
为便于讲解,我们把代码分成四个部分:
第一部分:定义了默认使用的串口终端设备路径及其它一些变量。
第二部分:根据main是否有输入参数确认使用哪个设备路径,并通过open的O_RDWR读写模式打开该设备。
第三部分:定义了一个结构体termios用于获取、设置终端设备的参数,包括波特率、数据位数、校验位等, 这是本章的重点,在下一小节详细说明。
第四部分:在while循环中对终端设备使用read和write进行读写,从而控制串口收发数据。 代码中在接收到的内容末尾加了’0’结束符,主要是为了方便使用字符串的方式处理内容。
11.5.2.1. termios结构体¶
示例代码中的第三部分,使用了termios结构体,它是在POSIX规范中定义的标准接口。 Linux系统利用termios来设置串口的参数,它是在头文件<termios.h>包含的<bits/termios.h>中定义的, 该文件中还包含了各个结构体成员可使用的宏值, 请自己使用locate命令查找该文件打开来阅读,关于termios结构体的定义摘录如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 | struct termios {
tcflag_t c_iflag; /* input mode flags */
tcflag_t c_oflag; /* output mode flags */
tcflag_t c_cflag; /* control mode flags */
tcflag_t c_lflag; /* local mode flags */
cc_t c_line; /* line discipline */
cc_t c_cc[NCCS]; /* control characters */
speed_t c_ispeed; /* input speed */
speed_t c_ospeed; /* output speed */
#define _HAVE_STRUCT_TERMIOS_C_ISPEED 1
#define _HAVE_STRUCT_TERMIOS_C_OSPEED 1
};
|
下面我们介绍一下各个结构体成员,主要是关注c_iflag、c_cflag以及c_ispeed、c_ospeed即可:
c_iflag:输入(input)模式标志,用于控制如何对串口输入的字符进行处理,常用的选项值见下表。
表 c_iflag选项值
选项值 |
作用 |
---|---|
INPCK |
启用输入奇偶检测 |
IGNPAR |
忽略帧错误和奇偶检验错误 |
IGNCR |
忽略输入中的回车 |
IXON |
开启XON/XOFF流控制 |
IXOFF |
关闭XON/XOFF流控制 |
c_oflag:输出(output)模式标志,用于控制串口的输出模式,常用的选项值见下表。
表 c_oflag选项值
选项值 |
作用 |
---|---|
ONLCR |
将输出中的换行符NL映射为回车-换行CR |
OCRNL |
将输出的回车映射为换行符 |
ONLRET |
不输出回车 |
OFILL |
发送填充字符串 |
c_cflag:控制(control)模式标志,用于控制串口的基本参数,如数据位、停止位等, 常用配置见下表,特别地,c_cflag结构体成员还包含了波特率的参数。
表 c_cflag选项值
选项值 |
作用 |
---|---|
CSIZE |
设置数据位长度,可以配置为CS5、CS6、CS7、CS8。 |
CSTOPB |
如果设置 CSTOPB 标志,则使用两位停止位 |
PARENB |
使能奇偶检验 |
PARODD |
设置为奇校验 |
c_lflag:本地(local)模式标志,主要用于控制驱动程序与用户的交互,在串口通信中, 实际上用不到该成员变量。
选项值 |
作用 |
---|---|
ISIG |
如果设置 ISIG 标志,当接收到字符INTR、QUIT等字符,系统会产生相应的信号。 |
ECHO |
是否需要回显字符 |
ICANON |
若设置了 ICANON 标志,则表示终端处于规范式输入状态,允许使用特殊字符EOF、KILL等 |
ECHONL |
若该标志位和ICANON标志位同时被设置,则回显换行符NL |
c_cc[NCCS]:该数组包含了终端的所有特殊字符, 可以修改特殊字符对应的键值(Ctrl+C产生的^C,ASCII码为0x03),部分内容如下表。
表 c_cc中各成员对应的下标值
数组的下标值 |
作用 |
---|---|
VINTR |
中断字符,若接收到该字符时,会发送SIGINT信号。当设置了c_lflag的ISIG标志位时,该字母不再作为输入传递。 |
VERASE |
删除字符,删除上一个字符。 |
VIM |
设置非标准模式读取的最小字节数 |
VTIM |
设置非标准模式读取时的延时值,单位为十分之一秒。 |
c_ispeed和c_ospeed:记录串口的输入和输出波特率(input speed和output speed), 部分可取值如下代码所示,宏定义中的数字以“0”开头,在C语言中这是表示8进制数字的方式。
1 2 3 4 5 6 7 8 | //注意以0开头的数字在是C语言的8进制数字形式
#define B1200 0000011
#define B1800 0000012
#define B2400 0000013
#define B4800 0000014
#define B9600 0000015
#define B19200 0000016
#define B38400 0000017
|
宏定义:termios结构体内部有_HAVE_STRUCT_TERMIOS_C_ISPEED和_HAVE_STRUCT_TERMIOS_C_OSPEED 两个宏定义,它们的宏值都为1,表示它支持c_ispeed和c_ospeed表示方式, 部分标准中不支持使用这两个结构体成员表示波特率,而只使用c_cflag来表示。
直接看结构体的定义比较抽象,下面我们以修改串口波特率、数据位、校验位和停止位的示例代码进行讲解。 接下来几个小节的代码,是我们从配套代码仓库/linux_app/tty/c_full/sources/bsp_uart.c文件截取的, 该文件以比较完善的方式封装了串口的配置,而本书提取出了代码中的重点进行分析, 感兴趣的读者可以打开配套的工程文件阅读。
11.5.2.1.1. 配置串口波特率¶
修改终端串口波特率的示例代码如下所示。
1 2 3 4 5 6 7 8 9 10 11 | //定义termios型变量opt
struct termios opt;
//fd是使用open打开设备文件得到的文件句柄
// 获取串口参数opt
tcgetattr(fd, &opt);
//设置串口输出波特率
cfsetospeed(&opt, B9600);
//设置串口输入波特率
cfsetispeed(&opt, B9600);
//更新配置
tcsetattr(fd, TCSANOW, &opt);
|
代码中使用到了头文件termios.h的库函数tcgetattr、cfsetispeed、cfsetospeed和tcsetattr。
其中tcgetattr和tcsetattr函数分别用于读取和设置串口的参数,原型如下:
1 2 3 4 5 6 7 | #include <termios.h>
#include <unistd.h>
int tcgetattr(int fd, struct termios *termios_p);
int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);
|
形参fd:指定串口设备文件的文件描述符。
形参termios_p:指向串口参数的结构体termios,tcgetattr读取到的参数会保存在该结构体中, 而tcsetattr则根据该结构体配置设备参数。
形参optional_actions:仅tcsetattr函数有这个参数,它用于指示配置什么时候生效, 它支持的配置参数如下:
TCSANOW表示立即生效。
TCSADRAIN表示待所有数据传输结束后配置生效。
TCSAFLUSH表示输入输出缓冲区为空时配置有效。
跟示例代码中的一样,通常都使用选项TCSANOW,让写入的参数配置立马生效。
代码中的cfsetispeed和cfsetospeed函数分别用于设置termios结构体的输入和输出波特率, 另外还有cfsetspeed函数可以同时设置输入和输出波特率参数为相同的值,原型如下:
1 2 3 4 5 | int cfsetispeed(struct termios *termios_p, speed_t speed);
int cfsetospeed(struct termios *termios_p, speed_t speed);
int cfsetspeed(struct termios *termios_p, speed_t speed);
|
使用这些函数要注意两点:
speed参数需要使用类似前面代码定义的宏值。
这三个函数只是修改了termios的opt变量的内容,并没有写入到设备文件, 因此在修改完它的内容后,还需要调用tcsetattr函数,把opt变量中的配置写入到设备,使它生效。
这就是修改终端设备参数的过程,读取原配置、修改termios参数、写入termios参数。
11.5.2.1.2. 配置串口停止位¶
c_cflag中的标志位CSTOPB,用于设置串口通信停止位的长度。若该值为0, 则停止位的长度为1位;若设置该位为1,则停止位的长度为两位,具体实现如下所示。
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 | //在bits/termios.h文件中关于CSTOPB的定义
//注意以0开头的数字在是C语言的8进制数字形式
#define CSTOPB 0000100
//
//设置停止位示例
//定义termios型变量opt
struct termios opt;
// 获取串口参数opt
tcgetattr(fd, &opt);
/* 设置停止位*/
switch (stopbits)
{
//设置停止位为1位
case 1:
opt.c_cflag &= ~CSTOPB;
break;
//设置停止位为2位
case 2:
opt.c_cflag |= CSTOPB;
break;
}
//更新配置
tcsetattr(fd, TCSANOW, &opt);
|
示例代码依然是采取了获取当前参数、修改配置、更新配置的套路。
修改配置的代码中使用了 &=~
、|=
这种位操作方法,主要是为了避免影响到变量中的其它位,
因为在c_cflag的其它位还包含了校验位、数据位和波特率相关的配置,如果直接使用“=”赋值,
那其它配置都会受到影响,而且操作不方便。在后面学习裸机开发,对寄存器操作时会经常用到这种方式。
若没接触过这些位操作方式,可参考本书附录中《第65章 位操作方法》的说明。
简单来说,示例中的 &=~
把c_cflag变量中CSTOPB对应的数据位清0,
而 |=
则把c_cflag变量中CSTOPB对应的数据位置1,
达到在不影响其它配置的情况下把停止位配置为1位或两位。
11.5.2.1.3. 配置串口校验位¶
配置串口的校验位涉及到termios成员c_cflag的标志位PARENB、PARODD 以及c_iflag的标志位INPCK, 其中PARENB和INPCK共同决定是否使能奇偶校验,而PARODD 决定使用奇校验还是偶校验, 配置的示例代码如下所示。
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 | //bits/termios.h的位定义
//注意以0开头的数字在是C语言的8进制数字形式
/* c_cflag bit meaning */
#define PARENB 0000400
#define PARODD 0001000
/* c_iflag bits */
#define INPCK 0000020
//
//定义termios型变量opt
struct termios opt;
// 获取串口参数opt
tcgetattr(fd, &opt);
switch (parity)
{
case 'n':
case 'N':
options.c_cflag &= ~PARENB; /* 不使用奇偶校验 */
options.c_iflag &= ~INPCK; /* 禁止输入奇偶检测 */
break;
case 'o':
case 'O':
options.c_cflag |= PARENB; /* 启用奇偶效验 */
options.c_iflag |= INPCK; /* 启用输入奇偶检测 */
options.c_cflag |= PARODD ; /* 设置为奇效验 */
break;
case 'e':
case 'E':
options.c_cflag |= PARENB; /* 启用奇偶效验 */
options.c_iflag |= INPCK; /* 启用输入奇偶检测 */
options.c_cflag &= ~PARODD; /* 设置为偶效验*/
break;
}
//更新配置
tcsetattr(fd, TCSANOW, &opt);
|
配置非常简单,不校验时同时把PARENB和INPCK位清零,启用校验时把PARENB和INPCK同时置1, 而PARODD为1时指定为奇校验,为0时是偶校验。
11.5.2.1.4. 配置串口数据位¶
串口的数据位是由c_cflag中的CSIZE配置的,由于串口支持5、6、7、8位的配置,一共有四种, 所以在c_cflag中使用了两个数据位进行配置,在配置前我们需要先对CSIZE数据位清零, 然后再赋予5、6、7、8的宏配置值,具体代码如下所示。
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 | //bits/termios.h的位定义
//注意以0开头的数字在是C语言的8进制数字形式
#define CSIZE 0000060
#define CS5 0000000
#define CS6 0000020
#define CS7 0000040
#define CS8 0000060
//
//定义termios型变量opt
struct termios opt;
// 获取串口参数opt
tcgetattr(fd, &opt);
//先清除CSIZE数据位的内容
opt.c_cflag &= ~CSIZE;
switch (databits) /*设置数据位数*/
{
case 5:
opt.c_cflag |= CS5;
break;
case 6:
opt.c_cflag |= CS6;
break;
case 7:
opt.c_cflag |= CS7;
break;
case 8:
opt.c_cflag |= CS8;
break;
}
//更新配置
tcsetattr(fd, TCSANOW, &opt);
|
学习了使用termios结构体配置串口参数的各种方式后,请再回过头看看前面的main.c示例文件代码, 相信已经不用再介绍了。
11.6. ioctl系统调用¶
通过前面的学习我们已经掌握了配置串口参数的方法,就是对设备文件操作,前面代码中使用到的文件操作摘录如下所示。
1 2 3 4 5 6 7 | //前面实验中对设备文件操作的函数
fd = open(path, O_RDWR);
write(fd, buf, strlen(buf));
read(fd, buf, 1024);
close(fd);
tcgetattr(fd, &opt);
tcsetattr(fd, TCSANOW, &opt);
|
仔细分析这些操作,发现万里晴空出现了两朵乌云。open、write、read、close都是Linux的系统调用, 而tcgetattr、tcsetattr则是库函数。而且按照传统的认知,文件操作大都是跟内容挂勾的, 上一章节的input事件设备文件记录了上报的事件信息,而tty设备的文件却不是记录串口终端的配置参数, 因为对文件的write操作是对外发送数据,而read则是读取接收到的数据, 也就是说,“tty*”文件并没有记录串口终端的配置信息, 那么tcgetattr、tcsetattr这两个函数究竟做了什么神仙操作?
它们实际上都是对ioctl系统调用的封装。
11.6.1. ioctl原型¶
ioctl系统调用的功能是向设备文件发送命令,控制一些特殊操作,它的函数原型如下:
1 2 | #include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
|
参数fd:与write、read类似,fd文件句柄指定要操作哪个文件。
参数reques:操作请求的编码,它是跟硬件设备驱动相关的,不同驱动设备支持不同的编码, 驱动程序通常会使用头文件提供可用的编码给上层用户。
参数“…”:这是一个没有定义类型的指针,它与printf函数定义中的“…”类似, 不过ioctl此处只能传一个参数。部分驱动程序执行操作请求时可能需要配置参数, 或者操作完成时需要返回数据,都是通过此处传的指针进行访问的。
11.6.2. 使用ioctl代替tcgetattr和tcsetattr¶
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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | #include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <termios.h>
#include <string.h>
#include <sys/ioctl.h>
//根据具体的设备修改
const char default_path[] = "/dev/ttyS3";
int main(int argc, char *argv[])
{
int fd;
int res;
struct termios opt;
char *path;
char buf[1024] = "Embedfire tty send test.\n";
//若无输入参数则使用默认终端设备
if(argc > 1)
path = argv[1];
else
path = (char *)default_path;
//获取串口设备描述符
printf("This is tty/usart demo.\n");
fd = open(path, O_RDWR);
if(fd < 0){
printf("Fail to Open %s device\n", path);
return 0;
}
//清空串口接收缓冲区
tcflush(fd, TCIOFLUSH);
// 获取串口参数opt
// tcgetattr(fd, &opt);
res = ioctl(fd,TCGETS, &opt);
opt.c_ispeed = opt.c_cflag & (CBAUD | CBAUDEX);
opt.c_ospeed = opt.c_cflag & (CBAUD | CBAUDEX);
//输出宏定义的值,方便对比
printf("Macro B9600 = %#o\n",B9600);
printf("Macro B115200 = %#o\n",B115200);
//输出读取到的值
printf("ioctl TCGETS,opt.c_ospeed = %#o\n", opt.c_ospeed);
printf("ioctl TCGETS,opt.c_ispeed = %#o\n", opt.c_ispeed);
printf("ioctl TCGETS,opt.c_cflag = %#x\n", opt.c_cflag);
speed_t change_speed = B9600;
if(opt.c_ospeed == B9600)
change_speed = B115200;
//设置串口输出波特率
cfsetospeed(&opt, change_speed);
//设置串口输入波特率
cfsetispeed(&opt, change_speed);
//设置数据位数
opt.c_cflag &= ~CSIZE;
opt.c_cflag |= CS8;
//校验位
opt.c_cflag &= ~PARENB;
opt.c_iflag &= ~INPCK;
//设置停止位
opt.c_cflag &= ~CSTOPB;
//更新配置
// tcsetattr(fd, TCSANOW, &opt);
res = ioctl(fd,TCSETS, &opt);
//再次读取
res = ioctl(fd,TCGETS, &opt);
opt.c_ispeed = opt.c_cflag & (CBAUD | CBAUDEX);
opt.c_ospeed = opt.c_cflag & (CBAUD | CBAUDEX);
printf("ioctl TCGETS after TCSETS\n");
//输出读取到的值
printf("ioctl TCGETS,opt.c_ospeed = %#o\n", opt.c_ospeed);
printf("ioctl TCGETS,opt.c_ispeed = %#o\n", opt.c_ispeed);
printf("ioctl TCGETS,opt.c_cflag = %#x\n", opt.c_cflag);
do{
//发送字符串
write(fd, buf, strlen(buf));
//接收字符串
res = read(fd, buf, 1024);
if(res >0 ){
//给接收到的字符串加结束符
buf[res] = '\0';
printf("Receive res = %d bytes data: %s\n",res, buf);
}
}while(res >= 0);
printf("read error,res = %d",res);
close(fd);
return 0;
}
|
本实验代码就是直接通过ioctl系统调用代替了tcgetattr和tcsetattr这两个库函数。
在示例代码中的第41行和76行,使用ioctl向设备文件发送了“TCGETS”请求, 在tty设备的驱动层,会根据这个请求返回配置参数,并通过传入的&opt指针传出。
类似地,示例代码中的第73行,使用ioctl向设备文件发送了“TCSETS”请求, 在tty设备的驱动层,会根据这个请求设置由指针&opt传入的配置参数,修改设备的属性。
特别地,由于使用ioctl获取配置参数时, 波特率的值不会直接写入到termios结构体的c_ispeed和c_ospeed成员,需要通过c_cflag的值运算得出, 所以第43、44行和78、79行加入了运算转换,运算出来的值是B9600或B115200之类的值。 如果不进行这样的运算操作,c_ispeed和c_ospeed得到的值可能是不对的。
代码的其它部分是输出的一些调试信息,方便在实验时验证获取到的信息是否正确。
关于ioctl的TCGETS和TCSETS参数,可以在man手册中查看,使用如下命令:
1 | man ioctl_tty
|
如下图:
ioctl系统调用应用非常广泛,因为并不是所有设备都仅有读写操作,例如控制CD-ROM的弹出和收回, 特殊设备的机械操作,又或者我们自己编写LED驱动程序也可以对上层提供指令实现花式点灯, 在以后编写驱动程序时,我们再来学习与ioctl系统调用相关的接口。
11.6.4. 运行¶
1 2 3 4 5 6 7 8 9 10 | #查看当前的波特率
stty -F /dev/ttyS3
#运行程序
sudo ./uart_i
Ctrl + C 退出程序
#查看当前波特率
stty -F /dev/ttyS3
|
运行结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | cat@lubancat:~/23/lubancat-test/base_linux/uart$ sudo stty -F /dev/ttyS3
speed 9600 baud; line = 0;
-brkint -imaxbel
cat@lubancat:~/23/lubancat-test/base_linux/uart$ sudo ./uart_i
This is tty/usart demo.
Macro B9600 = 015
Macro B115200 = 010002
ioctl TCGETS,opt.c_ospeed = 015
ioctl TCGETS,opt.c_ispeed = 015
ioctl TCGETS,opt.c_cflag = 0xcbd
ioctl TCGETS after TCSETS
ioctl TCGETS,opt.c_ospeed = 010002
ioctl TCGETS,opt.c_ispeed = 010002
ioctl TCGETS,opt.c_cflag = 0x1cb2
^C
cat@lubancat:~/23/lubancat-test/base_linux/uart$ sudo stty -F /dev/ttyS3
speed 115200 baud; line = 0;
-brkint -imaxbel
cat@lubancat:~/23/lubancat-test/base_linux/uart$
|
11.7. 查看glibc源码¶
如果你问我,是怎么知道这些的tcgetattr.c和tcsetattr.c是通过ioctl系统调用实现的, 又是如何知道c_ispeed和c_ospeed需要通过c_cflag成员运算得出的?答案是查看源码, 我们一直在强调Linux是开放的,就看我们如何挖掘这些宝藏了。
既然它们是库函数,那我们就到glibc的源码中找找,glibc的源码可以到其官网下载:http://www.gnu.org/software/libc/, 下载到源码后,使用VS Code编辑器的搜索功能,就可以搜到相关的内容,如下图所示。
这两个函数的定义位于glibc源码的如下目录: glibc/sysdeps/unix/sysv/linux/, 该目录中的tcgetattr.c和tcsetattr.c文件分别定义了这两个函数。
tcsetattr.c文件的内容如下。
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 | int
__tcsetattr (int fd, int optional_actions, const struct termios *termios_p)
{
struct __kernel_termios k_termios;
unsigned long int cmd;
switch (optional_actions)
{
case TCSANOW:
cmd = TCSETS;
break;
case TCSADRAIN:
cmd = TCSETSW;
break;
case TCSAFLUSH:
cmd = TCSETSF;
break;
default:
return INLINE_SYSCALL_ERROR_RETURN_VALUE (EINVAL);
}
k_termios.c_iflag = termios_p->c_iflag & ~IBAUD0;
k_termios.c_oflag = termios_p->c_oflag;
k_termios.c_cflag = termios_p->c_cflag;
k_termios.c_lflag = termios_p->c_lflag;
k_termios.c_line = termios_p->c_line;
#if _HAVE_C_ISPEED && _HAVE_STRUCT_TERMIOS_C_ISPEED
k_termios.c_ispeed = termios_p->c_ispeed;
#endif
#if _HAVE_C_OSPEED && _HAVE_STRUCT_TERMIOS_C_OSPEED
k_termios.c_ospeed = termios_p->c_ospeed;
#endif
memcpy (&k_termios.c_cc[0], &termios_p->c_cc[0],
__KERNEL_NCCS * sizeof (cc_t));
return INLINE_SYSCALL (ioctl, 3, fd, cmd, &k_termios);
}
libc_hidden_def (__tcgetattr)
weak_alias (__tcgetattr, tcgetattr)
|
代码看起来有点复杂,但刚接触的时候我们不需要完全弄清楚它的所有细节:
第7行:它使用ioctl发送了TCGETS请求,并传入了&k_termios指针记录参数。
第10行开始,都是把读取到的k_termios内容复制到__tcgetattr传入的termios_p中指针指向的变量中, 而c_ispeed和c_ospeed就是在第19、26行通过c_cflag运算得到的,所以我们使用tcgetattr库函数的时候, 不需要自己再运算赋值,而自己通过ioctl读取配置时则要加上运算转换。
glibc源码中使用了很多特别的宏或封装,如INLINE_SYSCALL、__glibc_likely、weak_alias等,感兴趣的读者可以查阅glibc官方的相关文档学习。