
⚫ 串口终端设备节点/dev/ttymxcX:对于 ALPHA/Mini I.MX6U 开发板来说,有两个串口,也就是有两个串口终端,对应两个设备节点,如下所示:

串口应用编程(API是C库函数,针对所有终端设备)
struct termios 结构体 (输入设备是struct input_event 结构体)
对于终端来说,其应用编程内容无非包括两个方面的内容:配置和读写;对于配置来说,一个很重要的数据结构便是 struct termios 结构体,该数据结构描述了终端的配置信息,这些参数能够控制、影响终端的行为、特性,事实上,终端设备应用编程(串口应用编程)主要就是对这个结构体进行配置。
示例代码 26.1.1 struct 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 */
};
⚫ 输入模式;⚫ 输出模式;⚫ 控制模式;⚫ 本地模式;⚫ 线路规程;⚫ 特殊控制字符;⚫ 输入速率;⚫ 输出速率。
一、输入模式:c_iflag
可用于 c_iflag 成员的宏如下所示:
IGNBRK 忽略输入终止条件
BRKINT 当检测到输入终止条件时发送 SIGINT 信号
IGNPAR 忽略帧错误和奇偶校验错误
PARMRK 对奇偶校验错误做出标记
INPCK 对接收到的数据执行奇偶校验
ISTRIP 将所有接收到的数据裁剪为 7 比特位、也就是去除第八位
INLCR 将接收到的 NL(换行符)转换为 CR(回车符)
IGNCR 忽略接收到的 CR(回车符)
ICRNL 将接收到的 CR(回车符)转换为 NL(换行符)
IUCLC 将接收到的大写字符映射为小写字符
IXON 启动输出软件流控
IXOFF 启动输入软件流控
以上所列举出的这些宏,我们可以通过 man 手册查询到它们的详细描述信息,执行命令" man 3 termios
"
二、输出模式:c_oflag
OPOST 启用输出处理功能,如果不设置该标志则其他标志都被忽略
OLCUC 将输出字符中的大写字符转换成小写字符
ONLCR 将输出中的换行符(NL '\n')转换成回车符(CR '\r')
OCRNL 将输出中的回车符(CR '\r')转换成换行符(NL '\n')
ONOCR 在第 0 列不输出回车符(CR)
ONLRET 不输出回车符
OFILL 发送填充字符以提供延时
OFDEL 如果设置该标志,则表示填充字符为 DEL 字符,否则为 NULL 字符
CBAUD 波特率的位掩码
B0 波特率为 0
…… ……
B1200 1200 波特率
B1800 1800 波特率
B2400 2400 波特率
B4800 4800 波特率
B9600 9600 波特率
B19200 19200 波特率
B38400 38400 波特率
B57600 57600 波特率
B115200 115200 波特率
B230400 230400 波特率
B460800 460800 波特率
B500000 500000 波特率
B576000 576000 波特率
B921600 921600 波特率
B1000000 1000000 波特率
B1152000 1152000 波特率
B1500000 1500000 波特率
B2000000 2000000 波特率
B2500000 2500000 波特率
B3000000 3000000 波特率
…… ……
CSIZE 数据位的位掩码
CS5 5 个数据位
CS6 6 个数据位
CS7 7 个数据位
CS8 8 个数据位
CSTOPB 2 个停止位,如果不设置该标志则默认是一个停止位
CREAD 接收使能
PARENB 使能奇偶校验
PARODD 使用奇校验、而不是偶校验
HUPCL 关闭时挂断调制解调器
CLOCAL 忽略调制解调器控制线
CRTSCTS 使能硬件流控
ISIG 若收到信号字符(INTR、QUIT 等),则会产生相应的信号
ICANON 启用规范模式
ECHO 启用输入字符的本地回显功能。当我们在终端输入字符的时候,字符
会显示出来,这就是回显功能
ECHOE 若设置 ICANON,则允许退格操作
ECHOK 若设置 ICANON,则 KILL 字符会删除当前行
ECHONL 若设置 ICANON,则允许回显换行符
ECHOCTL 若设置 ECHO,则控制字符(制表符、换行符等)会显示成“^X”
,
其中 X 的 ASCII 码等于给相应控制字符的 ASCII 码加上 0x40。例如,
退格字符(0x08)会显示为“^H”('H'的 ASCII 码为 0x48)
ECHOPRT 若设置 ICANON 和 IECHO,则删除字符(退格符等)和被删除的字
符都会被显示
ECHOKE 若设置 ICANON,则允许回显在 ECHOE 和 ECHOPRT 中设定的 KILL
字符
NOFLSH 在通常情况下,当接收到 INTR、QUIT 和 SUSP 控制字符时,会清空
输入和输出队列。如果设置该标志,则所有的队列不会被清空
TOSTOP 若一个后台进程试图向它的控制终端进行写操作,则系统向该后台进
程的进程组发送 SIGTTOU 信号。该信号通常终止进程的执行
IEXTEN 启用输入处理功能
⚫ VEOF:文件结尾符 EOF,对应键为 Ctrl+D;该字符使终端驱动程序将输入行中的全部字符传递给
正在读取输入的应用程序。如果文件结尾符是该行的第一个字符,则用户程序中的 read 返回 0,表
示文件结束。
⚫ VEOL:附加行结尾符 EOL,对应键为 Carriage return(CR);作用类似于行结束符。
⚫ VEOL2:第二行结尾符 EOL2,对应键为 Line feed(LF);
⚫ VERASE:删除操作符 ERASE,对应键为 Backspace(BS);该字符使终端驱动程序删除输入行中
的最后一个字符;
⚫ VINTR:中断控制字符 INTR,对应键为 Ctrl+C;该字符使终端驱动程序向与终端相连的进程发送
SIGINT 信号;
⚫ VKILL:删除行符 KILL,对应键为 Ctrl+U,该字符使终端驱动程序删除整个输入行;
⚫ VMIN:在非规范模式下,指定最少读取的字符数 MIN;
⚫ VQUIT:退出操作符 QUIT,对应键为 Ctrl+Z;该字符使终端驱动程序向与终端相连的进程发送
SIGQUIT 信号。
⚫ VSTART:开始字符 START,对应键为 Ctrl+Q;重新启动被 STOP 暂停的输出。
⚫ VSTOP:停止字符 STOP,对应键为 Ctrl+S;字符作用“截流”,即阻止向终端的进一步输出。用
于支持 XON/XOFF 流控。
⚫ VSUSP:挂起字符 SUSP,对应键为 Ctrl+Z;该字符使终端驱动程序向与终端相连的进程发送
SIGSUSP 信号,用于挂起当前应用程序。
⚫ VTIME:非规范模式下,指定读取的每个字符之间的超时时间(以分秒为单位)TIME。
ter.c_iflag |= (IGNBRK | BRKINT | PARMRK | ISTRIP);
规范模式
非规范模式下
termios_p->c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP| INLCR | IGNCR | ICRNL | IXON);termios_p->c_oflag &= ~OPOST;termios_p->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);termios_p->c_cflag &= ~(CSIZE | PARENB);termios_p->c_cflag |= CS8;
int fd;fd = open("/dev/ttymxc2", O_RDWR | O_NOCTTY);if (0 > fd) {perror("open error");return -1;}
#include <termios.h>#include <unistd.h>int tcgetattr(int fd, struct termios *termios_p);需要包含 termios.h 头文件和 unistd.h 头文件。第一个参数对应串口终端设备的文件描述符 fd。调用 tcgetattr 函数之前,我们需要定义一个 struct termios 结构体变量,将该变量的指针作为 tcgetattr() 函数的第二个参数传入函数调用成功返回 0;失败将返回-1,并且会设置 errno 以告知错误原因。
struct termios old_cfg;
if (0 > tcgetattr(fd, &old_cfg)) {
/* 出错处理 */
do_something();
}
struct termios new_cfg;memset(&new_cfg, 0x0, sizeof(struct termios)); //配置为原始模式cfmakeraw(&new_cfg);这个函数没有返回值。
new_cfg.c_cflag |= CREAD; //接收使能
cfsetispeed(&new_cfg, B115200);cfsetospeed(&new_cfg, B115200);B115200 是一个宏,前面已经给大家介绍了,B115200 表示波特率为 115200。
cfsetspeed(&new_cfg, B115200);
new_cfg.c_cflag &= ~CSIZE;new_cfg.c_cflag |= CS8; //设置为 8 位数据位
//奇校验使能new_cfg.c_cflag |= (PARODD | PARENB);new_cfg.c_iflag |= INPCK;//偶校验使能new_cfg.c_cflag |= PARENB;new_cfg.c_cflag &= ~PARODD; /* 清除 PARODD 标志,配置为偶校验 */new_cfg.c_iflag |= INPCK;//无校验new_cfg.c_cflag &= ~PARENB;new_cfg.c_iflag &= ~INPCK;
// 将停止位设置为一个比特new_cfg.c_cflag &= ~CSTOPB;// 将停止位设置为 2 个比特new_cfg.c_cflag |= CSTOPB;
new_cfg.c_cc[VTIME] = 0;new_cfg.c_cc[VMIN] = 0;
#include <termios.h>#include <unistd.h>int tcdrain(int fd);int tcflush(int fd, int queue_selector);int tcflow(int fd, int action);
⚫ TCOOFF:暂停数据输出(输出传输);⚫ TCOON:重新启动暂停的输出;⚫ TCIOFF:发送 STOP 字符,停止终端设备向系统发送数据;⚫ TCION:发送一个 START 字符,启动终端设备向系统发送数据;
⚫ TCIFLUSH:对接收到而未被读取的数据进行清空处理;⚫ TCOFLUSH:对尚未传输成功的输出数据进行清空处理;⚫ TCIOFLUSH:包括前两种功能,即对尚未处理的输入/输出数据进行清空处理。以上这三个函数,调用成功时返回 0;失败将返回-1、并且会设置 errno 以指示错误类型
通常我们会选择 tcdrain()或 tcflush()函数来对串口缓冲区进行处理。譬如直接调用 tcdrain()阻塞:
tcdrain(fd);
或者调用 tcflush()清空缓冲区:
tcflush(fd, TCIOFLUSH);
操作五 写入配置、使配置生效:tcsetattr()函数
#include <termios.h>#include <unistd.h>int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);调用该函数会将参数 termios_p 所指 struct termios 对象中的配置参数写入到终端设备中,使配置生效而参数 optional_actions 可以指定更改何时生效,其取值如下:⚫ TCSANOW:配置立即生效。⚫ TCSADRAIN:配置在所有写入 fd 的输出都传输完毕之后生效。⚫ TCSAFLUSH:所有已接收但未读取的输入都将在配置生效之前被丢弃。
操作六 读写数据:read()、write()
串口应用编程实战
#define _GNU_SOURCE //在源文件开头定义_GNU_SOURCE 宏
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <termios.h>
typedef struct uart_hardware_cfg {
unsigned int baudrate; /* 波特率 */
unsigned char dbit; /* 数据位 */
char parity; /* 奇偶校验 */
unsigned char sbit; /* 停止位 */
} uart_cfg_t;
static struct termios old_cfg; //用于保存终端的配置参数
static int fd; //串口终端对应的文件描述符
/**
** 串口初始化操作
** 参数 device 表示串口终端的设备节点
**/
static int uart_init(const char *device)
{
/* 打开串口终端 */
fd = open(device, O_RDWR | O_NOCTTY);
if (0 > fd) {
fprintf(stderr, "open error: %s: %s\n", device, strerror(errno));
return -1;
}
/* 获取串口当前的配置参数 备份数据*/
if (0 > tcgetattr(fd, &old_cfg)) {
fprintf(stderr, "tcgetattr error: %s\n", strerror(errno));
close(fd);
return -1;
}
return 0;
}
//////////////////////////////////////////////////////////////////////
/**
** 串口配置
** 参数 cfg 指向一个 uart_cfg_t 结构体对象
**/
static int uart_cfg(const uart_cfg_t *cfg)
{
struct termios new_cfg = {0}; //将 new_cfg 对象清零
speed_t speed;
/* 设置为原始模式 */
cfmakeraw(&new_cfg);
/* 使能接收 */
new_cfg.c_cflag |= CREAD;
/* 设置波特率 */
switch (cfg->baudrate) {
case 1200: speed = B1200;
break;
case 1800: speed = B1800;
break;
case 2400: speed = B2400;
break;
case 4800: speed = B4800;
break;
case 9600: speed = B9600;
break;
case 19200: speed = B19200;
break;
case 38400: speed = B38400;
break;
case 57600: speed = B57600;
break;
case 115200: speed = B115200;
break;
case 230400: speed = B230400;
break;
case 460800: speed = B460800;
break;
case 500000: speed = B500000;
break;
default: //默认配置为 115200
speed = B115200;
printf("default baud rate: 115200\n");
break;
}
if (0 > cfsetspeed(&new_cfg, speed)) {
fprintf(stderr, "cfsetspeed error: %s\n", strerror(errno));
return -1;
}
/* 设置数据位大小 */
new_cfg.c_cflag &= ~CSIZE; //将数据位相关的比特位清零
switch (cfg->dbit) {
case 5:
new_cfg.c_cflag |= CS5;
break;
case 6:
new_cfg.c_cflag |= CS6;
break;
case 7:
new_cfg.c_cflag |= CS7;
break;
case 8:
new_cfg.c_cflag |= CS8;
break;
default: //默认数据位大小为 8
new_cfg.c_cflag |= CS8;
printf("default data bit size: 8\n");
break;
}
/* 设置奇偶校验 */
switch (cfg->parity) {
case 'N': //无校验
new_cfg.c_cflag &= ~PARENB;
new_cfg.c_iflag &= ~INPCK;
break;
case 'O': //奇校验
new_cfg.c_cflag |= (PARODD | PARENB);
new_cfg.c_iflag |= INPCK;
break;
case 'E': //偶校验
new_cfg.c_cflag |= PARENB;
new_cfg.c_cflag &= ~PARODD; /* 清除 PARODD 标志,配置为偶校验 */
new_cfg.c_iflag |= INPCK;
break;
default: //默认配置为无校验
new_cfg.c_cflag &= ~PARENB;
new_cfg.c_iflag &= ~INPCK;
printf("default parity: N\n");
break;
}
/* 设置停止位 */
switch (cfg->sbit) {
case 1: //1 个停止位
new_cfg.c_cflag &= ~CSTOPB;
break;
case 2: //2 个停止位
new_cfg.c_cflag |= CSTOPB;
break;
default: //默认配置为 1 个停止位
new_cfg.c_cflag &= ~CSTOPB;
printf("default stop bit size: 1\n");
break;
}
/* 将 MIN 和 TIME 设置为 0 */
new_cfg.c_cc[VTIME] = 0;
new_cfg.c_cc[VMIN] = 0;
/* 清空缓冲区 */
if (0 > tcflush(fd, TCIOFLUSH)) {
fprintf(stderr, "tcflush error: %s\n", strerror(errno));
return -1;
}
/* 写入配置、使配置生效 */
if (0 > tcsetattr(fd, TCSANOW, &new_cfg)) {
fprintf(stderr, "tcsetattr error: %s\n", strerror(errno));
return -1;
}
/* 配置 OK 退出 */
return 0;
}
////////////////////////////////////////////////////////////////////////////////////
/***
--dev=/dev/ttymxc2
--brate=115200
--dbit=8
--parity=N
--sbit=1
--type=read
***/
/**
** 终端打印帮助信息
**/
static void show_help(const char *app)
{
printf("Usage: %s [选项]\n"
"\n 必选选项:\n"
" --dev=DEVICE 指定串口终端设备名称, 譬如--dev=/dev/ttymxc2\n"
" --type=TYPE 指定操作类型, 读串口还是写串口, 譬如--type=read(read 表示读、write 表示写、其它值无效)\n"
"\n 可选选项:\n"
" --brate=SPEED 指定串口波特率, 譬如--brate=115200\n"
" --dbit=SIZE 指定串口数据位个数, 譬如--dbit=8(可取值为: 5/6/7/8)\n"
" --parity=PARITY 指定串口奇偶校验方式, 譬如--parity=N(N 表示无校验、O 表示奇校验、E 表示偶校验)\n"
" --sbit=SIZE 指定串口停止位个数, 譬如--sbit=1(可取值为: 1/2)\n"
" --help 查看本程序使用帮助信息\n\n", app);
}
////////////////////////////////////////////////////////////////////////////////////
/**
** 信号处理函数,当串口有数据可读时,会跳转到该函数执行
**/
static void io_handler(int sig, siginfo_t *info, void *context)
{
unsigned char buf[10] = {0};
int ret;
int n;
if(SIGRTMIN != sig)
return;
/* 判断串口是否有数据可读 */
if (POLL_IN == info->si_code) {
ret = read(fd, buf, 8); //一次最多读 8 个字节数据
printf("[ ");
for (n = 0; n < ret; n++)
printf("0x%hhx ", buf[n]);
printf("]\n");
}
}
////////////////////////////////////////////////////////////////////////////////
/**
** 异步 I/O 初始化函数
**/
static void async_io_init(void)
{
struct sigaction sigatn;
int flag;
/* 使能异步 I/O */
flag = fcntl(fd, F_GETFL); //使能串口的异步 I/O 功能
flag |= O_ASYNC;
fcntl(fd, F_SETFL, flag);
/* 设置异步 I/O 的所有者 */
fcntl(fd, F_SETOWN, getpid());
/* 指定实时信号 SIGRTMIN 作为异步 I/O 通知信号 */
fcntl(fd, F_SETSIG, SIGRTMIN);
/* 为实时信号 SIGRTMIN 注册信号处理函数 */
sigatn.sa_sigaction = io_handler; //当串口有数据可读时,会跳转到 io_handler 函数
sigatn.sa_flags = SA_SIGINFO;
sigemptyset(&sigatn.sa_mask);
sigaction(SIGRTMIN, &sigatn, NULL);
}
//////////////////////////////////////////////////////////////////////////////
//主函数
int main(int argc, char *argv[])
{
uart_cfg_t cfg = {0};
char *device = NULL;
int rw_flag = -1;
unsigned char w_buf[10] = {0x11, 0x22, 0x33, 0x44,0x55, 0x66, 0x77, 0x88};
//通过串口发送出去的数据
int n;
/* 解析出参数 */
for (n = 1; n < argc; n++) {
if (!strncmp("--dev=", argv[n], 6))
device = &argv[n][6];
else if (!strncmp("--brate=", argv[n], 8))
cfg.baudrate = atoi(&argv[n][8]);
else if (!strncmp("--dbit=", argv[n], 7))
cfg.dbit = atoi(&argv[n][7]);
else if (!strncmp("--parity=", argv[n], 9))
cfg.parity = argv[n][9];
else if (!strncmp("--sbit=", argv[n], 7))
cfg.sbit = atoi(&argv[n][7]);
else if (!strncmp("--type=", argv[n], 7)) {
if (!strcmp("read", &argv[n][7]))
rw_flag = 0; //读
else if (!strcmp("write", &argv[n][7]))
rw_flag = 1; //写
}
else if (!strcmp("--help", argv[n])) {
show_help(argv[0]); //打印帮助信息
exit(EXIT_SUCCESS);
}
}
if (NULL == device || -1 == rw_flag) {
fprintf(stderr, "Error: the device and read|write type must be set!\n");
show_help(argv[0]);
exit(EXIT_FAILURE);
}
/* 串口初始化 */
if (uart_init(device))
exit(EXIT_FAILURE);
/* 串口配置 */
if (uart_cfg(&cfg)) {
tcsetattr(fd, TCSANOW, &old_cfg); //恢复到之前的配置
close(fd);
exit(EXIT_FAILURE);
}
/* 读|写串口 */
switch (rw_flag) {
case 0: //读串口数据
async_io_init(); //我们使用异步 I/O 方式读取串口的数据,调用该函数去初始化串口的异步 I/O
for ( ; ; )
sleep(1); //进入休眠、等待有数据可读,有数据可读之后就会跳转到 io_handler()函数
break;
case 1: //向串口写入数据
for ( ; ; ) { //循环向串口写入数据
write(fd, w_buf, 8); //一次向串口写入 8 个字节
sleep(1); //间隔 1 秒钟
}
break;
}
/* 退出 */
tcsetattr(fd, TCSANOW, &old_cfg); //恢复到之前的配置,这跟单片机不同
close(fd);
exit(EXIT_SUCCESS);
}
在开发板上进行测试

执行测试程序后,测试程序会每隔 1 秒中将 8 个字节数据[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88] 写入到 RS232 串口,此时 PC 端串口调试助手便会接收到这些数据,如下所示: