本次实验先简单的通过一个c脚本来实现串口数据的收发,我们只需要用到open、read、write等系统调用就能轻易实现;
实验代码分析
#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/ttymxc2";
// const char default_path[] = "/dev/ttymxc2";
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’结束符,主要是为了方便使用字符串的方式处理内容。
termios结构体
示例代码中的第三部分,使用了termios结构体,它是在POSIX规范中定义的标准接口。Linux系统利用termios来设置串口的参数,它是在头文件<termios.h>包含的<bits/termios.h>中定义的,该文件中还包含了各个结构体成员可使用的宏值,请自己使用locate命令查找该文件打开来阅读,关于termios结构体的定义摘录如下所示。
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进制数字的方式。
//注意以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来表示。
直接看结构体的定义比较抽象,下面我们以修改串口波特率、数据位、校验位和停止位的示例代码进行讲解。接下来代码,是我们从上面文件截取的,该文件以比较完善的方式封装了串口的配置,而提取出了代码中的重点进行分析。
配置串口波特率
修改终端串口波特率的示例代码如下所示
//定义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函数分别用于读取和设置串口的参数,原型如下:
#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函数可以同时设置输入和输出波特率参数为相 同的值,原型如下:
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参数。
配置串口停止位
c_cflag中的标志位CSTOPB,用于设置串口通信停止位的长度。若该值为0,则停止位的长 度为1位;若设置该位为1,则停止位的长度为两位,具体实现如下所示。
//在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位或两位。
配置串口校验位
配置串口的校验位涉及到termios成员c_cflag的标志位PARENB、PARODD 以及c_iflag的标志 位INPCK,其中PARENB和INPCK共同决定是否使能奇偶校验,而PARODD 决定使用奇校验还是偶校验,配置的示例代码如下所示。
//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时是偶校验。
配置串口数据位
串口的数据位是由c_cflag中的CSIZE配置的,由于串 口支持5、6、7、8位的配置,一共有四种,所以在c_cflag中使用了两个数据 位进行配置,在配置前我们需要先对CSIZE数据位清零,然后再赋予5、6、7、8的宏配置值,具体代码如下所示。
//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示例文件代码,相信已经不用再介绍了。
编译及测试
本实验使用的Makefile相对于前面的章节仅修改了最终的可执行文件名为tty_demo。
对于ARM架构的程序,可使用如下步骤进行编译:
#在主机的实验代码Makefile目录下编译
#编译arm平台的程序
make ARCH=arm
编译后生成的ARM平台程序为build_arm/tty_demo,使用网络文件系统
共享至开发板,程序默认使用ttyS1通讯,也可以自行指定输入设备路径。
#以下命令在Ubuntu主机执行
#根据自己主机的设备配置minicom,注意波特率要匹配
#关于minicom的配置请参考前面小节的内容
sudo minicom /dev/ttyS1
#配置完毕打开minicom终端
#以下命令在开发板上的终端执行
#在NFS共享的工程目录路径执行
#使用默认的ttymxc2设备通讯
./build_arm/tty_demo
#程序会通过串口发送内容至主机
#主机使用minicom可发送内容至开发板
