RK3588是一款低功耗、高性能的处理器,适用于基于arm的PC和Edge计算设备、个人移动互联网设备等数字多媒体应用,RK3588支持8K视频编解码,内置GPU可以完全兼容OpenGLES 1.1、2.0和3.2。RK3588引入了新一代完全基于硬件的最大4800万像素ISP,内置NPU,支持INT4/INT8/INT16/FP16混合运算能力,支持安卓12和、Debian11、Build root、Ubuntu20和22版本登系统。了解更多信息可点击迅为官网
【粉丝群】824412014
【实验平台】:迅为RK3588开发板
【内容来源】《iTOP-3588开发板系统编程手册》
【全套资料及网盘获取方式】联系淘宝客服加入售后技术支持群内下载
【视频介绍】:【强者之芯】 新一代AIOT高端应用芯片 iTOP -3588人工智能工业AI主板
第16章 串口应用编程
串口设备是嵌入式开发中最常用的外设之一,通过串口打印信息可以调试程序的运行,通过串口也可以链接很多种外设,比如串口打印机,蓝牙,wifi,GPS,GPRS 等等。
16.1串口介绍
串口是一种通过串行通信传输数据的接口,与并行接口不同,它只使用一个信号线路传输数据,这样就可以避免并行接口由于多条线路的干扰和延迟等问题而导致的错误。在计算机和外部设备之间,通过串口可以传输数据、控制信号和时钟等信息,是计算机通信中最常用的接口之一。
下面对串口通信相关的知识进行简要介绍:
1.数据传输方式
串口通信使用的是串行传输方式,即将数据按照顺序一个一个地发送或接收。每个数据位都在一定的时间间隔内发送或接收。串口使用两条数据线(TXD,RXD)和一条地线(GND)进行传输。
2.数据格式
串口通信使用的是异步传输方式,数据被分成固定大小的数据帧,每个数据帧由起始位、数据位、校验位和停止位组成,通常称为“帧”。其中起始位和停止位用于标识数据帧的开始和结束,校验位用于检验数据传输的准确性。数据位的数量一般为7位或8位。
3.波特率
波特率是指数据传输的速度,也称为比特率,表示每秒钟可以传输多少个二进制数据位。波特率越高,数据传输的速度就越快。常见的波特率有9600bps、19200bps、38400bps、115200bps等。
4.硬件流控制和软件流控制
为了保证数据传输的准确性和稳定性,串口通信还需要使用流控制。硬件流控制是通过硬件电路控制数据的传输,软件流控制则是通过软件控制数据的传输。硬件流控制的主要方法是使用RTS和CTS两个信号线进行控制,软件流控制的主要方法是使用XON和XOFF两个控制字符进行控制。
5.错误检测
在串口通信过程中,常常会出现一些错误,例如校验错误、帧错误、溢出错误等。为了避免这些错误,通常使用校验位来检测数据的准确性,并使用缓冲区来避免数据溢出。
6.串口编程
为了控制串口通信,我们需要进行串口编程。在Linux系统中,可以使用文件I/O操作和ioctl操作来进行串口编程。文件I/O操作可以用于读取和写入串口数据,ioctl操作可以用于设置串口参数、控制流控制和获取串口状态等操作(关于ioctl相关的指令宏会在之后的小节进行讲解)。
16.2串口设备节点介绍
在嵌入式设备中,串口设备通常以字符设备节点的形式出现在Linux系统中。在Linux系统中,每个设备都由一个设备节点(device node)来表示,设备节点是与设备相关联的一个文件,以/dev目录下的文件形式存在。
串口设备节点通常以tty开头,具体命名方式根据串口的类型和数量不同而不同。开发板系统启动之后,使用以下命令打印终端设备节点,如下图所示:
ls /dev/tty*
dev/ttyX(X 是一个数字编号,譬如 0、1、2、3 等)设备节点:tty是teletype 的简称,在 Linux 中,/dev/ttyX 代表的都是本地终端, Linux 内核在初始化时所生成的 63 个本地终端,包括/dev/tty1~/dev/tty63 一共63 个本地终端,可以是连接到开发板的LCD 显示器、键盘和鼠标等。
串口终端设备节点:从开发板原理图可以了解到,在iTOP-3588开发板上UART2为串口调试终端,对应的设备节点为/dev/ttyFIQ0,
底板上485对应/dev/ttyS0。
基于USB的虚拟串口:ttyGS0以及ttyUSBX(X 是一个数字编号,譬如 0、1、2、3 等)都是USB的虚拟串口,其中ttyGS0为烧写usb虚拟出的串口,在系统启动之后可以在windows终端通过“adb shell”命令进入开发板控制台。ttyUSBX在这里为4G模块的虚拟串口。
16.3串口的使用步骤
步骤 | 解释 |
1 | 打开串口设备文件 open() |
2 | 获取当前串口的配置参数 tcgetattr() |
3 | 修改串口的配置参数 tcsetattr() |
4 | 写入修改后的配置参数 tcsetattr() |
5 | 对串口数据进行读写 read() /write() |
6 | 关闭串口设备文件 close() |
关于打开和关闭设备文件我们已经非常熟悉了,接下来将对上述重要步骤和函数进行讲解。
(1)获取当前串口的配置参数
使用tcgetattr()函数可以获取当前串口的配置参数,包括串口的波特率、数据位、停止位、校验位等信息。在对串口进行应用编程时,需要了解当前串口的配置参数,才能进行相应的操作,比如修改串口的波特率或其他配置参数。同时,获取当前串口属性还可以用于备份当前的配置参数,便于在操作完成后还原串口属性,以避免对其他程序或系统造成影响。因此,使用tcgetattr()函数获取串口属性是进行串口编程的基础。tcgetattr()函数所需头文件和函数原型如下所示:
所需头文件 | 函数原型 | |
1 2 | #include <termios.h> #include <unistd.h> | int tcgetattr(int fd, struct termios *termios_p); |
该函数的作用是获取与指定终端相关的参数(也称为终端属性或终端设置),并将这些参数存储到termios结构体中。其中,fd参数是打开的串口设备文件的文件描述符,termios_p参数是一个指向termios结构体的指针,用于存储从终端读取的属性。
termios结构体包含了终端的所有属性信息,包括输入输出波特率、数据位、校验位、停止位等。其定义如下:
struct termios {
tcflag_t c_iflag; // 输入模式标志
tcflag_t c_oflag; // 输出模式标志
tcflag_t c_cflag; // 控制模式标志
tcflag_t c_lflag; // 本地模式标志
cc_t c_cc[NCCS]; // 控制字符数组
};
其中,c_iflag、c_oflag、c_cflag和c_lflag分别表示输入模式标志、输出模式标志、控制模式标志和本地模式标志。c_cc数组是一组控制字符,包含了终端驱动程序使用的一些控制字符(会在下一小节对该结构体进行详细讲解)。
(2)修改和写入串口的配置参数
tcsetattr()函数是Linux中设置终端属性的函数,它用于设置指定终端设备的属性。
所需头文件 | 函数原型 | |
1 2 | #include <termios.h> #include <unistd.h> | int tcsetattr(int fd, int optional_actions, const struct termios *termios_p); |
接下来对tcsetattr函数要用到的三个参数进行介绍:
参数名称 | 参数含义 | |
1 | filename | 打开的终端设备的文件描述符。 |
2 | optional_actions | 可选操作,决定何时应用改变的属性值。常见的可选操作包括以下三个值: (1)TCSANOW:立即改变属性值。 (2)TCSADRAIN:等待所有输出数据传输完毕后改变属性值。 (3)TCSAFLUSH:清空输入和输出缓冲区,并等待数据传输完毕后改变属性值。 |
3 | termios_p | 一个指向终端属性结构体的指针,用于存储新的终端属性值 |
在调用tcsetattr()函数时,需要提供一个指向termios结构体的指针。这个结构体包含了串口的各种参数,如波特率、数据位数、停止位数、奇偶校验等(关于各种参数的修改会在17.5小节进行详细的讲解)。
16.4.1输入模式
输入模式控制输入数据(终端驱动程序从串口或键盘接收到的字符数据)在被传递给应用程序之前的处理方式。通过设置 struct termios 结构体中 c_iflag 成员的标志对它们进行控制。所有的标志都被定义为宏, 除 c_iflag 成员外,c_oflag、c_cflag 以及 c_lflag 成员也都采用这种方式进行配置。可用于 c_iflag 成员的宏如下所示:
成员 | 对应成员的含义 |
IGNBRK | 忽略输入终止条件 |
BRKINT | 当检测到输入终止条件时发送 SIGINT 信号 |
IGNPAR | 忽略帧错误和奇偶校验错误 |
PARMRK | 对奇偶校验错误做出标记 |
INPCK | 对接收到的数据执行奇偶校验 |
ISTRIP | 将所有接收到的数据裁剪为 7 比特位、也就是去除第八位 |
INLCR | 将接收到的 NL(换行符)转换为 CR(回车符) |
IGNCR | 忽略接收到的 CR(回车符) |
ICRNL | 将接收到的 CR(回车符)转换为 NL(换行符) |
IUCLC | 将接收到的大写字符映射为小写字符 |
IXON | 启动输出软件流控 |
16.4.2输出模式
输出模式控制输出字符的处理方式,即由应用程序发送出去的字符数据在传递到串口或屏幕之前是如何处理的。可用于 c_oflag 成员的宏如下所示:
成员 | 对应成员的含义 |
OPOST | 启用输出处理功能,如果不设置该标志则其他标志都被忽略 |
OLCUC | 将输出字符中的大写字符转换成小写字符 |
ONLCR | 将输出中的换行符(NL '\n')转换成回车符(CR '\r') |
OCRNL | 将输出中的回车符(CR '\r')转换成换行符(NL '\n') |
ONOCR | 在第 0 列不输出回车符(CR |
ONLRET | 不输出回车符 |
OFILL | 发送填充字符以提供延时 |
OFDEL | 如果设置该标志,则表示填充字符为 DEL 字符,否则为 NULL字符 |
16.4.3控制模式
在这个结构体中,最终要的就是c_cflag ,可以控制模式控制终端设备的硬件特性,譬如对于串口来说,该字段比较重要,可设置串口波特率、数据位、校验位、停止位等硬件特性。通过设置 struct termios 结构中 c_cflag 成员的标志对控制模式进行配置。可用于 c_cflag 成员的标志如下所示:
c_cflag支持的常量名称 | |
CBAUD | 波特率的位掩码 |
B0 | 0波特率(放弃DTR) |
B1800 | 1800波特率 |
B2400 | 2400波特率 |
B4800 | 4800波特率 |
B9600 | 9600波特率 |
B19200 | 19200波特率 |
B38400 | 38400波特率 |
B57600 | 57600波特率 |
B115200 | 115200波特率 |
CSIZE | 数据位的位掩码 |
CS5 | 5个数据位 |
CS6 | 6个数据位 |
CS7 | 7个数据位 |
CS8 | 8个数据位 |
CSTOPB | 2个停止位(不设则是1个停止位) |
CREAD | 接收使能 |
PARENB | 校验位使能 |
PARODD | 使用奇校验而不使用偶校验 |
HUPCL | 最后关闭时挂线(放弃DTR) |
CLOCAL | 本地连接(不改变端口所有者) |
LOBLK | 块作业控制输出 |
CNET_CTSRTS | 硬件流控制使能 |
16.4.4本地模式
本地模式用于控制终端的本地数据处理和工作模式。通过设置 struct termios 结构体中 c_lflag 成员的标 志对本地模式进行配置。可用于 c_lflag 成员的标志如下所示:
c_iflag支持的常量名称 | |
INPCK | 奇偶校验使能 |
IGNPAR | 忽略奇偶校验错误 |
PARMRK | 奇偶校验错误掩码 |
ISTRIP | 除去奇偶校验位 |
IXON | 启动出口硬件流控 |
IXOFF | 启动入口软件流控 |
IXANY | 允许字符重新启动流控 |
IGNBRK | 忽略中断情况 |
BRKINT | 当发生中断时发送SIGINT信号 |
INLCR | 将NL映射到CR |
IGNCR | 忽略CR |
ICRNL | 将CR映射到NL |
ICANON | 启用规范模式 |
16.4.5特殊控制字符
特殊控制字符是一些字符组合,如 Ctrl+C、Ctrl+Z 等,当用户键入这样的组合键,终端会采取特殊处理方式。struct termios 结构体中 c_cc 数组将各种特殊字符映射到对应的支持函数。每个字符位置(数组下标)由对应的宏定义的,如下所示
c_cc 支持的常量名称 | |
VKILL | 删除行,对应键为CTRL+U |
VEOF | 位于文件结尾,对应键为CTRL+D |
VEOL | 位于行尾,对应键为Carriage return(CR) |
VEOL2 | 位于第二行尾,对应键为Line feed(LF) |
VMIN | 指定了最少读取的字符数 |
VTIME | 指定了读取每个字符的等待时间 |
VINTR | 中断控制,对应键为CTRL+C |
VQUIT | 退出操作,对应键为CRTL+Z |
VERASE | 删除操作,对应键为Backspace(BS) |
在以上所列举的这些宏定义中,TIME 和 MIN 值只能用于非规范模式,可用于控制非规范模式下 read()调用的一些行为特性,后面再向大家介绍。
16.5对串口进行配置
16.5.1设置串口的波特率
在编写串口应用程序时,设置串口波特率是必须的步骤之一,它决定了数据传输的速率。在 Linux 中,设置波特率通常使用 cfsetspeed() 函数,cfsetspeed()函数所需头文件和函数原型如下所示:
所需头文件 | 函数原型 | |
1 2 | #include <termios.h> #include <unistd.h> | int cfsetspeed(struct termios *termios_p, speed_t speed); |
它接受一个 struct termios 结构体作为输入参数,同时返回一个整数值,表示操作是否成功。该函数实际上是对 c_cflag 字段中的波特率进行设置。例如,如果要将波特率设置为 115200,可以调用以下代码:
struct termios options;
// 获取当前终端配置
tcgetattr(fd, &options);
// 设置波特率为 115200
cfsetspeed(&options, B115200);
// 将新的终端配置写入终端
tcsetattr(fd, TCSANOW, &options);
需要注意以下几点:
(1)需要先打开串口设备文件并且获取串口的属性,包括波特率在内的其他属性。可以使用 tcgetattr() 函数获取属性值,并存储在一个 termios 结构体变量中。
(2)设置波特率时需要调用 cfsetspeed() 函数,并传入一个波特率常量作为参数。常用的波特率常量包括 B9600、B115200 等。这些常量可以在头文件 termios.h 中找到。
(3)设置完成后,需要使用 tcsetattr() 函数将属性值写回到串口设备中。
16.5.2设置数据位大小
在串口通讯中,数据位指的是每个字符(byte)中实际包含的数据位数。通常情况下,一个字符包含8个位(即8个0或1),但有时也可能为7个位或其他值。
在编写串口应用程序时,需要使用 struct termios 结构体中的 c_cflag 成员变量来设置数据位大小。具体来说,需要将 c_cflag 成员变量中与数据位相关的位清零,然后再根据需要设置相应的值。
清零操作通常使用按位与(&)运算符和位运算中的“非”运算符(~),具体步骤如下:
new_cfg.c_cflag &= ~CSIZE; // 将数据位相关的比特位清零
其中,CSIZE 是一个宏定义,表示数据位的位掩码。宏定义通常定义在 termios.h 头文件中,其取值如下:
#define CSIZE 0x00000300 /* 字符长度掩码 */
通过清零操作,将数据位相关的位全部置为0。接下来,可以使用按位或(|)运算符和宏定义来设置具体的数据位数,例如:
new_cfg.c_cflag |= CS8; // 将数据位数设置为8位
此时,CS8 宏定义将会被解释为一个包含8位的位掩码,通过按位或运算,将其设置到 c_cflag 成员变量中,从而完成数据位的设置。
16.5.3设置奇偶校验位
串口的奇偶校验位配置一共涉及到 struct termios 结构体中的两个成员变量:c_cflag 和 c_iflag。首先对于 c_cflag 成员,需要添加 PARENB 标志以使能串口的奇偶校验功能,只有使能奇偶校验功能之后才会对输出数据产生校验位,从而对输入数据进行校验检查;同时对于 c_iflag 成员来说,还需要添加 INPCK 标志,这样才能对接收到的数据执行奇偶校验,代码如下所示:
奇校验使能:
new_cfg.c_cflag |= (PARODD | PARENB); // 设置为奇校验
new_cfg.c_iflag |= INPCK; // 使能奇偶校验
偶校验使能:
new_cfg.c_cflag &= ~PARODD; // 设置为偶校验
new_cfg.c_cflag |= PARENB; // 使能奇偶校验
new_cfg.c_iflag |= INPCK; // 对输入数据执行奇偶校验
无校验:
new_cfg.c_cflag &= ~PARENB; // 禁用奇偶校验
new_cfg.c_iflag &= ~INPCK; // 不执行奇偶校验
16.5.4设置停止位
在串口通信中,停止位用于指定每个数据帧的结束位置。在传输一个完整的数据字节后,通常需要一个或多个停止位,以便接收端能够确定一个数据帧的结束。停止位的数量通常为1位或2位,其中1位停止位被广泛使用,而2位停止位则较少使用。
在Linux中,通过在 struct termios 结构体中设置 c_cflag 成员变量的 CSTOPB 标志位来控制停止位的数量。当 CSTOPB 为0时,仅使用1位停止位;当 CSTOPB 为1时,则使用2位停止位。
例如,以下代码将串口的停止位设置为1位:
1 | new_cfg.c_cflag &= ~CSTOPB; // 设置停止位为1位 |
16.6串口实验
本小节代码在配套资料“iTOP-3588开发板\03_【iTOP-RK3588开发板】指南教程\03_系统编程配套程序\66”目录下,如下图所示:
实验要求:
编写串口应用程序,实验发送和接收两种模式的测试。设置波特率为115200、8位数据位、1位停止位,无校验。main函数传递的第一个参数用来表示串口的设备节点,第二个参数来设置串口的收发模式,如果参数为send表示发送模式,来发送固定的字符,如果参数为rece表示接收模式,用来接收发送过来的数据。
16.6.1编写应用程序
实验步骤:
首先进入到ubuntu的终端界面输入以下命令来创建 demo66_uart.c文件,如下图所示:
vim demo102_uart.c
然后向该文件中添加以下内容:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <string.h>
#define BUFFER_SIZE 256 // 定义缓冲区大小为256字节
void set_serial_port(int fd, int baud_rate, int data_bits, int stop_bits, int parity)
// 设置串口的波特率、数据位、停止位和校验位
{
struct termios options;
tcgetattr(fd, &options); // 获取串口的设置选项
options.c_cflag &= ~CSIZE; // 设置数据位为8位
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // 关闭规范模式、回显和信号处理
options.c_oflag &= ~OPOST; // 关闭输出处理
options.c_iflag &= ~(IXON | IXOFF | IXANY); // 关闭输入处理
options.c_cc[VTIME] = 0; // 设置读取时的超时时间
options.c_cc[VMIN] = 0; // 设置读取时的最小字节数
switch (baud_rate)
// 设置波特率
{
case 9600:
cfsetispeed(&options, B9600);
cfsetospeed(&options, B9600);
break;
case 19200:
cfsetispeed(&options, B19200);
cfsetospeed(&options, B19200);
break;
case 38400:
cfsetispeed(&options, B38400);
cfsetospeed(&options, B38400);
break;
case 57600:
cfsetispeed(&options, B57600);
cfsetospeed(&options, B57600);
break;
case 115200:
cfsetispeed(&options, B115200);
cfsetospeed(&options, B115200);
break;
default:
cfsetispeed(&options, B115200);
cfsetospeed(&options, B115200);
break;
}
options.c_cflag |= CLOCAL | CREAD; // 必须设置的标志,表示启用本地连接和接收数据
options.c_cflag &= ~CRTSCTS; // 关闭硬件流控制
switch (parity)
// 设置校验位
{
case 'O': // 奇校验
options.c_cflag |= PARENB;
options.c_cflag |= PARODD;
options.c_iflag |= (INPCK | ISTRIP);
break;
case 'E': // 偶校验
options.c_cflag |= PARENB;
options.c_cflag &= ~PARODD;
options.c_iflag |= (INPCK | ISTRIP);
break;
case 'N': // 无校验
options.c_cflag &= ~PARENB;
break;
default:
options.c_cflag &= ~PARENB;
break;
}
// 设置数据位
switch (data_bits)
{
case 7:
options.c_cflag &= ~CSIZE; // 清除CSIZE位
options.c_cflag |= CS7; // 设置CS7位,即设置数据位为7位
break;
case 8:
options.c_cflag &= ~CSIZE; // 清除CSIZE位
options.c_cflag |= CS8; // 设置CS8位,即设置数据位为8位
break;
default:
options.c_cflag &= ~CSIZE; // 清除CSIZE位
options.c_cflag |= CS8; // 默认设置数据位为8位
break;
}
// 设置停止位
switch (stop_bits)
{
case 1:
options.c_cflag &= ~CSTOPB; // 清除CSTOPB位,即设置停止位为1位
break;
case 2:
options.c_cflag |= CSTOPB; // 设置CSTOPB位,即设置停止位为2位
break;
default:
options.c_cflag &= ~CSTOPB; // 默认设置停止位为1位
break;
}
tcsetattr(fd, TCSANOW, &options); // 把新的设置应用到串口上
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
printf("Usage: %s [serial device] [send|recv]\n", argv[0]);
return -1;
}
int fd;
char buffer[BUFFER_SIZE];
if (strcmp(argv[2], "send") == 0) // 发送数据
{
fd = open(argv[1], O_WRONLY | O_NOCTTY | O_SYNC);
if (fd < 0)
{
printf("Error opening serial device\n");
return -1;
}
set_serial_port(fd, 115200, 8, 1, 'N'); // 设置串口参数
while (1) // 不停地读取用户输入,并写入串口
{
printf("Enter message: ");
fgets(buffer, BUFFER_SIZE, stdin);
write(fd, buffer, strlen(buffer));
}
}
else if (strcmp(argv[2], "recv") == 0) // 接收数据
{
fd = open(argv[1], O_RDONLY | O_NOCTTY | O_SYNC);
if (fd < 0)
{
printf("Error opening serial device\n");
return -1;
}
set_serial_port(fd, 115200, 8, 1, 'N'); // 设置串口参数
while (1) // 不停地读取串口数据,并输出到屏幕上
{
int bytes_read = read(fd, buffer, BUFFER_SIZE);
if (bytes_read > 0)
{
buffer[bytes_read] = '\0';
printf("Received message: %s", buffer);
}
}
}
else // 参数不合法
{
printf("Invalid option\n");
return -1;
}
return 0;
}
保存退出之后,使用以下命令设置交叉编译器环境,并对demo102_uart.c进行交叉编译,编译完成如下图所示:
export PATH=/usr/local/arm64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin:$PATH
aarch64-none-linux-gnu-gcc -o demo66_uart demo66_uart.c
最后将交叉编译生成的demo66_uart文件拷贝到/home/nfs共享目录下即可。
16.6.2开发板测试
Buildroot系统启动之后,首先使用以下命令进行nfs共享目录的挂载(其中192.168.1.7为作者ubuntu的ip地址,需要根据自身ubuntu的ip来设置),如下图所示:
mount -t nfs -o nfsvers=3,nolock 192.168.1.7:/home/nfs /mnt
nfs共享目录挂载到了开发板的/mnt目录下,进入到/mnt目录下,如下图所示:
可以看到/mnt目录下的demo66_uart文件已经存在了,然后使用以下命令通过开发板的485(使用的uart0)向外发送数据(usb 转485已经被连接好了开发板和PC),如下图所示:
./demo66_uart /dev/ttyS0 send
在PC端打开串口助手,设置好相应的功能之后打开串口,如下图所示:
随后在开发板终端进行数据的发送,如下图所示:
串口助手接收成功之后如下所示:
串口发送测试完成之后,使用以下命令进行串口的接收测试,命令使用之后如下图所示:
./demo66_uart /dev/ttyS0 recv
然后在PC端使用串口助手,进行数据的发送,如下图所示:
串口助手每发送一个字符串,开发板的串口终端就会将对应的字符串打印出来,如下图所示:
至此我们串口的接收功能也测试完成了。