文章目录
- 一、TTY介绍
- (1)理解tty
- (2)tty设备节点
- 二、tty串口应用编程
- (1)串口基本操作
- 【1】打开串口
- 【1】读写数据
- 【1】关闭串口
- (2)termios 结构体(配置)
- (3)终端控制API函数
- (4)终端的三种工作模式
- 三、tty串口应用编程实现
- (1)硬件连接
- (2)应用编程代码
- (3)运行结果
- 【1】发送数据
- 【2】接收数据
- 四、Linux下异步I/O:O_ASYNC标志(信号驱动I/O)
- (1)信号驱动I/O
- (2)信号处理函数
一、TTY介绍
(1)理解tty
简单来说,tty 是 Teletype / Teletypewriter 的缩写。而 Teletype / Teletypewriter 的中文意思则是电传打字机。
大家可以参考这篇文章来理解:点击打开(对终端、命令行和shell等的理解)
终端 = tty = Teletype / Teletypewriter = 电传打字机(可以这样理解)
简单来说,tty 是终端的统称。早期的终端是电传字打印机(Teletype / Teletypewriter),英文缩写就是 tty。虽然终端设备已经不再限制于电传打字机了,但是 tty 这个名称还是就这么保留了下来。
(2)tty设备节点
我们到/dev/路径下看一下tty的设备节点,可以看见很多:
root@igkboard:~# ls /dev
autofs gpiochip1 loop0 mmcblk1boot0 pps1 ram14 rtc tty10 tty20 tty30 tty40 tty50 tty60 ttymxc6 vcs6 vcsu3
block gpiochip2 loop1 mmcblk1boot1 ptmx ram15 rtc0 tty11 tty21 tty31 tty41 tty51 tty61 ubi_ctrl vcsa vcsu4
bus gpiochip3 loop2 mmcblk1p1 ptp0 ram2 rtc1 tty12 tty22 tty32 tty42 tty52 tty62 udev_network_queue vcsa1 vcsu5
char gpiochip4 loop3 mmcblk1p2 ptp1 ram3 shm tty13 tty23 tty33 tty43 tty53 tty63 urandom vcsa2 vcsu6
console hwrng loop4 mmcblk1rpmb pts ram4 snd tty14 tty24 tty34 tty44 tty54 tty7 v4l vcsa3 vga_arbiter
cpu_dma_latency i2c-1 loop5 mqueue ram0 ram5 stderr tty15 tty25 tty35 tty45 tty55 tty8 vcs vcsa4 vhci
disk initctl loop6 mxc_asrc ram1 ram6 stdin tty16 tty26 tty36 tty46 tty56 tty9 vcs1 vcsa5 video0
fd input loop7 net ram10 ram7 stdout tty17 tty27 tty37 tty47 tty57 ttymxc0 vcs2 vcsa6 watchdog
full kmsg mapper null ram11 ram8 tty tty18 tty28 tty38 tty48 tty58 ttymxc1 vcs3 vcsu watchdog0
fuse log mem port ram12 ram9 tty0 tty19 tty29 tty39 tty49 tty59 ttymxc2 vcs4 vcsu1 zero
gpiochip0 loop-control mmcblk1 pps0 ram13 random tty1 tty2 tty3 tty4 tty5 tty6 ttymxc3 vcs5 vcsu2
这些节点的总结如下(注:表格中的 X 代表数字编号)
二、tty串口应用编程
(1)串口基本操作
【1】打开串口
打开串口连接的时候,程序在open函数中除了Read+Write模式以外还需要指定O_NOCTTY选项:
fd=open("/dev/ttymxc0",O_RDWR|O_NOCTTY);
标志O_NOCTTY告诉系统这个程序不会成为这个端口上的“控制终端”。如果不这样做的话,所有的输入,比如键盘上过来的Ctrl+C中止信号等等,会影响到你的进程。
【1】读写数据
读数据的时候需要找准时机,需要知道串口何时有数据,可以使用linux下的轮询机制进行监控串口的文件描述符:
rv = read(fd, buf, 1024);
Linux下一切皆文件,写数据直接使用write、fputs等函数即可直接向串口发送数据:
rv= write(fd, buf, sizeof(buf));
【1】关闭串口
可以使用close系统调用关闭串口:
close(fd);
(2)termios 结构体(配置)
termios是面向所有终端设备的。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_oflag:
输出模式标志,控制终端输出方式。
c_cflag:
控制模式标志,指定终端硬件控制信息.。
c_lflag:
本地模式标志,控制终端编辑功能。
c_cc[NCCS]:
控制字符,用于保存终端驱动程序中的 特殊字符,如输入结束符等。
(3)终端控制API函数
具体函数参数等可以自行man看看,后面的代码中涉及到的也可以拿来参考。
tcgetattr 取属性(termios结构)
tcsetattr 设置属性(termios结构)
cfgetispeed 得到输入速度
cfgetospeed 得到输出速度
cfsetispeed 设置输入速度
cfsetospeed 设置输出速度
tcdrain 等待所有输出都被传输
tcflow 挂起传输或接收
tcflush 刷清未决输入和/或输出
tcsendbreak 送BREAK字符
tcgetpgrp 得到前台进程组ID
tcsetpgrp 设置前台进程组ID
cfmakeraw 将终端设置成原始模式
cfsetspeed 设置输入输出速度
其中设置输入输出波特率的时候需要注意,波特率可以设置如下:
B0、B50、B75、B110、B134、B150、B200、B300、B600、B1200、B1800、B2400、B4800、B9600、B19200、B38400、 B57600、 B115200、 B230400
struct termios uart_cfg;
memset(&uart_cfg,0,sizeof(struct termios));
cfsetispeed(&uart_cfg,B115200); //设置输入波特率
cfsetospeed(&uart_cfg,B115200); //设置输出波特率
(4)终端的三种工作模式
终端的三种工作模式,分别是规范模式 canonical mode,非规范模式 non-canonical mode 和原始模式 raw mode。
通过设置 c_lflag 设置 ICANNON 标志来定义终端是以规范模式还是非规范模式工作,默认为规范模式。
- 规范模式
所有输入基于行进行处理。在用户输入一个行结束符(回车符、EOF【End Of File】等)之前,系统调用read()函数读不到用户输入的任何字符。其次,除了EOF之外的行结束符与普通字符一样会被read()函数读取到缓冲区中。一次调用read()只能读取一行数据。
- 原始模式
是一种特殊的非规范模式,所有的输入数据以字节为单位被处理。即有一个字节输入时,触发输入有效。
- 非规范模式
所有输入时即时有效的,不需要用户另外输入行结束符。
在非规范模式下,对参数 MIN(c_cc[VMIN])和 TIME(c_cc[VTIME])的设置决定 read 函数的调用方式,MIN 和 TIME 的取值不同,会有以下四种不同的情况:
三、tty串口应用编程实现
(1)硬件连接
(2)应用编程代码
/*********************************************************************************
* Copyright: (C) 2023 WangDengtao<1799055460@qq.com>
* All rights reserved.
*
* Filename: uart_test.c
* Description: This file
*
* Version: 1.0.0(2023年04月08日)
* Author: WangDengtao <1799055460@qq.com>
* ChangeLog: 1, Release initial version on "2023年04月08日 14时45分32秒"
*
********************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <getopt.h>
#include <libgen.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <signal.h>
#define READ_FLAG 0 /*读标志*/
#define WRITE_FLAG 1 /*写标志*/
struct uart_parameter {
unsigned int baudrate; // 波特率
unsigned char dbit; // 数据位
char parity; // 奇偶校验
unsigned char sbit; // 停止位
};
static struct termios oldtio; // 用于保存终端的配置参数
static int fd_uart; // 串口终端对应的文件描述符
static int uart_init(const char *device);//串口初始化
static int uart_configuration(const struct uart_parameter *para);//串口配置
static void async_io_init(void);//异步i/o初始化函数
static void io_handler(int sig, siginfo_t *info, void *context);//信号处理函数,当串口有数据可读时,会跳转到该函数执行
static void usage(char *progname);//提示信息
int main(int argc, char *argv[])
{
struct uart_parameter uart_para;
char device[64];
int rw_flag = -1;
unsigned char write_buf[10] = {0x11, 0x22, 0x33, 0x44,0x55, 0x66, 0x77, 0x88};
int n;
int opt;
char *progname=NULL;
memset(&uart_para, 0x0, sizeof(struct uart_parameter));
memset(device, 0x0, sizeof(device));
struct option long_options[] = {
{"device", required_argument, NULL, 'D'},
{"type", required_argument, NULL, 'T'},
{"brate", no_argument, NULL, 'b'},
{"dbit", no_argument, NULL, 'd'},
{"parity", no_argument, NULL, 'p'},
{"sbit", no_argument, NULL, 's'},
{"help", no_argument, NULL, 'h'},
{NULL, 0, NULL, 0}
};
memset(&uart_para, 0x0, sizeof(struct uart_parameter));
progname = (char *)basename(argv[0]);
while((opt = getopt_long(argc, argv, "D:T:b:d:p:s:h", long_options, NULL)) != -1)
{
switch(opt)
{
case'D':
strcpy(device, optarg);
break;
case'T':
if (!strcmp("read", optarg))
{
rw_flag = READ_FLAG;
}
else if (!strcmp("write", optarg))
{
rw_flag = WRITE_FLAG;
}
break;
case'b':
uart_para.baudrate = atoi(optarg);
break;
case'd':
uart_para.dbit = atoi(optarg);
break;
case'p':
uart_para.parity = *optarg;
break;
case's':
uart_para.sbit = atoi(optarg);
break;
case'h':
usage(progname);
return 0;
default:
break;
}
}
if (NULL == device || -1 == rw_flag)
{
usage(progname);
return -1;
}
/* 串口初始化 */
if (uart_init(device))
{
printf("fail to execute uart_init\n");
return -2;
}
/* 串口配置 */
if (uart_configuration(&uart_para))
{
/* 恢复之前的配置 */
tcsetattr(fd_uart, TCSANOW, &oldtio);
return -3;
}
/* 通过读写标志判断读写,然后进行读写 */
switch (rw_flag)
{
case 0: // 读串口数据
async_io_init(); // 我们使用异步 i/o 方式读取串口的数据,调用该函数去初始化串口的异步 i/o
for ( ; ; ) // 进入休眠,等待有数据可读,有数据可读之后就会跳转到 io_handler() 函数
{
sleep(1);
}
break;
case 1: // 向串口写入数据
for ( ; ; )
{
write(fd_uart, write_buf, 8);
sleep(1);
}
break;
}
tcsetattr(fd_uart, TCSANOW, &oldtio);
close(fd_uart);
return 0;
}
static int uart_init(const char *device)
{
fd_uart = open(device, O_RDWR | O_NOCTTY);
if (0 > fd_uart)
{
printf("fail to open uart file\n");
return -1;
}
/* 获取串口当前的配置参数 */
if (0 > tcgetattr(fd_uart, &oldtio))
{
printf("fail to get old attribution of terminal\n");
close(fd_uart);
return -2;
}
return 0;
}
static int uart_configuration(const struct uart_parameter *para)
{
struct termios newtio;
speed_t speed;
/* 设置为原始模式
* 配置为原始模式相当于已经对 newtio 做了如下配置
* IGNBRK 忽略输入终止条件,BRKINT 检测到终止条件发送 SIGINT 信号,PARMRK 对奇偶校验做出标记
* ISTRIP 裁剪数据位为 7 bit,去掉第八位,INLCR 换行符转换为回车符,IGNCR 忽略回车符
* ICRNL 将回车符转换为换行符,IXON 启动输出流控
* OPOST 启用输出处理功能
* ECHO 使能回显,ICANON 规范模式,ISIG 收到信号产生相应的信号,IEXTEN 输入处理
* CSIZE 数据位掩码,PARENB 使能校验,CS8 8 个数据位
* 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;
*/
memset(&newtio, 0x0, sizeof(struct termios));
cfmakeraw(&newtio);
/* CREAD 使能接受 */
newtio.c_cflag |= CREAD;
/* 设置波特率 */
switch (para->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:
speed = B115200;
printf("default baud rate is 115200\n");
break;
}
/* cfsetspeed 函数,设置波特率 */
if (0 > cfsetspeed(&newtio, speed))
{
printf("fail to set baud rate of uart\n");
return -1;
}
/* 设置数据位大小
* CSIZE 是数据位的位掩码,与上掩码的反,就是将数据位相关的比特位清零
* CSX (X=5,6,7,8) 表示数据位位数
*/
newtio.c_cflag &= ~CSIZE;
switch (para->dbit)
{
case 5:
newtio.c_cflag |= CS5;
break;
case 6:
newtio.c_cflag |= CS6;
break;
case 7:
newtio.c_cflag |= CS7;
break;
case 8:
newtio.c_cflag |= CS8;
break;
default:
newtio.c_cflag |= CS8;
printf("default data bit size is 8\n");
break;
}
/* 设置奇偶校验
* PARENB 用于使能校验
* INPCK 用于对接受的数据执行校验
* PARODD 指的是奇校验
*/
switch (para->parity)
{
case 'N': //无校验
newtio.c_cflag &= ~PARENB;
newtio.c_iflag &= ~INPCK;
break;
case 'O': //奇校验
newtio.c_cflag |= (PARODD | PARENB);
newtio.c_iflag |= INPCK;
break;
case 'E': //偶校验
newtio.c_cflag |= PARENB;
newtio.c_cflag &= ~PARODD;
newtio.c_iflag |= INPCK;
break;
default: //默认配置为无校验
newtio.c_cflag &= ~PARENB;
newtio.c_iflag &= ~INPCK;
printf("default parity is N (no check)\n");
break;
}
/* 设置停止位
* CSTOPB 表示设置两个停止位
*/
switch (para->sbit)
{
case 1: //1个停止位
newtio.c_cflag &= ~CSTOPB;
break;
case 2: //2个停止位
newtio.c_cflag |= CSTOPB;
break;
default: //默认配置为1个停止位
newtio.c_cflag &= ~CSTOPB;
printf("default stop bit size is 1\n");
break;
}
/* 将 MIN 和 TIME 设置为 0,通过对 MIN 和 TIME 的设置有四种 read 模式
* read 调用总是会立即返回,若有可读数据,则读数据并返回被读取的字节数,否则读取不到数据返回 0
*/
newtio.c_cc[VTIME] = 0;
newtio.c_cc[VMIN] = 0;
/* 清空输入输出缓冲区 */
if (0 > tcflush(fd_uart, TCIOFLUSH))
{
printf("fail to flush the buffer\n");
return -3;
}
/* 写入配置,使配置生效 */
if (0 > tcsetattr(fd_uart, TCSANOW, &newtio))
{
printf("fail to set new attribution of terminal\n");
return -4;
}
return 0;
}
/* 异步 i/o 初始化函数 */
static void async_io_init(void)
{
struct sigaction sigatn;
int flag;
/* 使能异步 i/o,获取当前进程状态,并开启当前进程异步通知功能 */
flag = fcntl(fd_uart, F_GETFL);
flag |= O_ASYNC;
fcntl(fd_uart, F_SETFL, flag);
/* 设置异步 i/o 的所有者,将本应用程序进程号告诉内核 */
fcntl(fd_uart, F_SETOWN, getpid());
/* 指定实时信号 SIGRTMIN 作为异步 i/o 通知信号 */
fcntl(fd_uart, F_SETSIG, SIGRTMIN);
/* 为实时信号 SIGRTMIN 注册信号处理函数
* 当串口有数据可读时,会跳转到 io_handler 函数
*/
sigatn.sa_sigaction = io_handler;
sigatn.sa_flags = SA_SIGINFO;
/* 初始化信号集合为空 */
sigemptyset(&sigatn.sa_mask);
/* sigaction 的功能是为信号指定相关的处理程序,但是它在执行信号处理程序时
* 会把当前信号加入到进程的信号屏蔽字中,从而防止在进行信号处理期间信号丢失
*/
sigaction(SIGRTMIN, &sigatn, NULL);
}
/* 信号处理函数,当串口有数据可读时,会跳转到该函数执行 */
static void io_handler(int sig, siginfo_t *info, void *context)
{
unsigned char buf[10];
int ret;
int n;
memset(buf, 0x0, sizeof(buf));
if(SIGRTMIN != sig)
{
return;
}
/* 判断串口是否有数据可读 */
if (POLL_IN == info->si_code)
{
ret = read(fd_uart, buf, 8);
printf("[ ");
for (n = 0; n < ret; n++)
{
printf("0x%hhx ", buf[n]);
}
printf("]\n");
}
}
static void usage(char *progname)
{
printf("Usage: %s [OPTION]...\n", progname);
printf("The usage has been configured\n");
printf("%s -D device -T write/read\n", progname);
printf("If you want a different configuration, see below:\n");
printf("-b brate\n");
printf("-d dbit\n");
printf("-p parity\n");
printf("-s sbit\n");
printf("-h help\n");
return;
}
Makefile:
CC=arm-linux-gnueabihf-gcc
APP_NAME=uart_test
all:clean
@${CC} ${APP_NAME}.c -o ${APP_NAME} -D_GNU_SOURCE
clean:
@rm -f ${APP_NAME}
然后将生成的可执行文件下载到我们的开发板上。
如果开发板上没有使能我们的设备树插件,需要使能,然后重启即可。
root@igkboard:/run/media/mmcblk1p1# cat config.txt
#Enble UART overlays
dtoverlay_uart=2 3 4 7
(3)运行结果
root@igkboard:~# tftp -gr uart_test 192.168.137.91
root@igkboard:~# chmod a+x uart_test
root@igkboard:~# ./uart_test -h
Usage: uart_test [OPTION]...
The usage has been configured
uart_test -D device -T write/read
If you want a different configuration, see below:
-b brate
-d dbit
-p parity
-s sbit
-h help
打开我们的串口调试助手,连接上与转换器的接口。
串口调试助手中需要选择HEX(16进制)显示,不然会显示乱码:串口的Hex/AscII发送与显示
【1】发送数据
在命令上执行:
./uart_test -D /dev/ttymxc1 -T write
【2】接收数据
在命令上执行:
./uart_test -D /dev/ttymxc1 -T read
四、Linux下异步I/O:O_ASYNC标志(信号驱动I/O)
在上面的代码中,我们用到了 O_ASYNC 标志来完成I/O异步通信。什么为异步I/O,假设两个设备通信,其中一个设备正在接受发送过来的数据,但是同时也需要自己发送出去,这样就叫异步I/O,如果同步的话,这个设备发送完数据之后去接受,数据已经错过了。
(1)信号驱动I/O
在IO多路复用中,进程是通过系统调用(select、epoll)来检测文件描述符上是否可以执行IO。而在信号驱动IO中,进程请求内核当文件描述符上可执行IO操作时为自己发送一个信号。之后进程就可以执行任何其他的任务直到IO就绪为止。下面来说明一下上述代码中所使用信号驱动IO步骤:
1、得到文件描述符的状态标志集,为该状态标志集添加一个 O_ASYNC 属性:
flag = fcntl(fd_uart, F_GETFL);
/*
后面两步可以简化为一步:
fcntl(fd_uart, F_SETFL, flag | O_ASYNC);
*/
flag |= O_ASYNC;
fcntl(fd_uart, F_SETFL, flag);
2、用fcntl函数来设置一个用来接受信号的进程:
fcntl(fd_uart, F_SETOWN, getpid());
3、指定实时信号 SIGRTMIN 作为异步 i/o 通知信号,默认情况下,这个通知信号为SIGIO:
sigatn.sa_sigaction = io_handler;
4、为信号设置一个处理函数,用来读取并处理位于输入缓存中的数据:
sigatn.sa_sigaction = io_handler;
(2)信号处理函数
/* 信号处理函数,当串口有数据可读时,会跳转到该函数执行 */
static void io_handler(int sig, siginfo_t *info, void *context)
{
unsigned char buf[10];
int ret;
int n;
memset(buf, 0x0, sizeof(buf));
if(SIGRTMIN != sig)
{
return;
}
/* 判断串口是否有数据可读 */
if (POLL_IN == info->si_code)
{
ret = read(fd_uart, buf, 8);
printf("[ ");
for (n = 0; n < ret; n++)
{
printf("0x%hhx ", buf[n]);
}
printf("]\n");
}
}
相信大家看不懂的也只有这一行:POLL_IN == info->si_code
我们来看一下 siginfo_t 结构体:
siginfo_t {
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused hardware-generated signal(unused on most
architectures) */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time consumed */
sigval_t si_value; /* Signal value */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count; POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
long si_band; /* Band event (was int in glibc 2.3.2 and earlier) */
int si_fd; /* File descriptor */
short si_addr_lsb; /* Least significant bit of address(since Linux 2.6.32) */
void *si_call_addr; /* Address of system call instruction(since Linux 3.5) */
int si_syscall; /* Number of attempted system call(since Linux 3.5) */
unsigned int si_arch; /* Architecture of attempted system call(since Linux 3.5) */
}
我们着重看 si_code: