1. 串口的基本认知
串行接口简称串口,也称串行通信接口或串行通讯接口(通常指COM接口),是采用串行通信方 式的扩展接口。串行接口(Serial Interface)是指数据一位一位地顺序传送。其特点是通信线路简 单,只要一对传输线就可以实现双向通信(可以直接利用电话线作为传输线),从而大大降低了成 本,特别适用于远距离通信,但传送速度较慢
面试的时候可能会问,有没有玩过spi,可以回答玩过串口和I2c
- 是设备间接线通信的一种方式
- 数据一位一位地顺序传送
- 双向通信,全双工
-
- 半双工:只能单向通讯
- 全双工:可以双向通讯
- 传送速度相对较慢
- 串口通信(Serial Communication)是一种在计算机和外部设备或计算机之间进行的串行数据传输方式。它采用逐位(bit)顺序传输数据,相对于并行通信而言,串口通信使用的数据线较少,在远距离通信中可以节约通信成本。串口通信广泛应用于各种设备之间的数据传输,如计算机与外设(如打印机、鼠标、键盘等)、微控制器之间的通信等。
- 串口通信的基本参数包括波特率(数据传输速率)、数据位(每个数据包的位数)、停止位(用于表示单个数据包的结束)、奇偶校验位(用于错误检测)等。这些参数需要在通信双方之间预先约定,以确保数据的正确传输和解析。
- 串口通信的主要优点包括使用简单的线路结构、成本较低、适用于远距离通信等。然而,它也有一些缺点,如传输速度相对较慢、传输距离受限等。因此,在选择通信方式时,需要根据具体的应用场景和需求进行权衡。
1.1. 关于电器标准和协议:
串行接口按电气标准及协议来分包括RS-232-C、RS-422、RS485等。RS-232-C、RS-422与RS-485标准只对接口的电气特性做出规定,不涉及接插件、电缆或协议。
RS-232
也称标准串口,最常用的一种[串行通讯接口,比如我们的电脑主机的9针串口 ,最高速率为20kb/s,RS-232是为点对点(即只用一对收、发设备)通讯而设计的,其传送距离最大为约15米。所以RS-232适合本地设备之间的通信
RS-422
由于接收器采用高输入阻抗和发送驱动器比RS232更强的驱动能力,故允许在相同传输线上连接多个接收节点,最多可接10个节点。即一个主设备(Master),其余为从设备(Slave),从设备之间不能通信,所以RS-422支持点对多的双向通信。
RS-422的最大传输距离为1219米,最大传输速率为10Mb/s。平衡双绞线的长度与传输速率成反比
RS-485
是从RS-422基础上发展而来的,无论四线还是二线连接方式总线上可多接到32个设备
1.2. 关于串口的电平:
经常听到的UART:
异步串行是指UART(Universal Asynchronous Receiver/Transmitter),通用异步接收/发送。UART包含TTL电平的串口和RS232电平的串口
RS232电平:
逻辑1为-3~-15V的电压,逻辑0为 3~15V的电压
笔记本通过RS232电平和单片机通信
TTL电平:
TTL是(Transistor-Transistor Logic),即晶体管-晶体管逻辑的简称,它是计算机处理器控制的设备 内部各部分之间通信的标准技术。TTL电平信号应用广泛,是因为其数据表示采用二进制规定, +5V等价于逻辑”1”,0V等价于逻辑”0”。
数字电路中,由TTL电子元器件组成电路的电平是个电压范围,规定: 输出高电平>=2.4V,输出低电平<=0.4V; 输入高电平>=2.0V,输入低电平<=0.8V
笔记本电脑通过TTL电平与单片机进行通信,需要将电脑的USB转成TTL电平,需要用到CH340工具将USB转换成TTL电平实现电脑与MCU之间的通信
1.3. 串口通信引脚接线:
RXD:数据接收引脚,
TXD:数据发送引脚
VCC:电源正极
GND:电源负极
1.4. 串口的通信协议:
串口通信协议中的波特率、奇偶检验位和停止位等参数是非常重要的,它们决定了数据的传输方式和数据的完整性检查。以下是一些常见的串口通信参数:
波特率(Baud Rate):
波特率是串口通信中的传输速率,它表示每秒传输的比特数。常见的波特率包括 9600、115200、57600 等。
通信的双方必须使用相同的波特率。波特率过高可能会导致数据传输错误,因此需要根据硬件和通信距离来选择适当的波特率。
数据位(Data Bits):
数据位指定每个数据字节中的位数,通常为 5、6、7 或 8 位。大多数情况下,使用 8 位数据位以支持 8 位的二进制数据。
奇偶检验位(Parity):
奇偶检验位用于检测数据传输中的错误。通常有以下几种选项: 无校验位:不使用奇偶检验位。 奇校验位:确保数据位中有奇数个 “1”。 偶校验位:确保数据位中有偶数个 “1”。 奇偶检验位通常用于检测单比特错误。
停止位(Stop Bits):
停止位表示数据字节的结束。通常有 1 位和 2 位停止位选项,其中 1 位停止位是最常见的选择。 这些参数通常一起组合,以确定数据的帧格式。例如,常见的串口通信设置是:
波特率:9600 数据位:8 无奇偶检验位 1 位停止位 这意味着每个数据帧由 10 位组成:1 位起始位、8 位数据位、无奇偶检验位和 1 位停止位。
正确配置这些参数对于串口通信非常重要,因为通信的双方必须使用相同的参数,否则会导致数据传输错误。通常,串口设备的规格表明了所需的通信参数设置。在编程中,你需要使用相应的库函数来设置这些参数,以确保正确的数据传输和校验。
2. Orangepi Zero 2串口通信开发
2.1. Orangepi Zero 2的两组串口:
Orangepi Zero2开发板有两组串口,第一种串口是开发板刚开始登录的时候用到的,还有一组可以通过输入:gpio readall来查询,都是用的TTL针脚型接口,串口接线:TXD和RXD交叉接线
2.2. 串口调试工具:
使用安信可串口调试助手,亲测好用,可以调节波特率,可以设置快捷键等。
2.3. 查看串口驱动文件:
/* 在配套的wiringPi库里面有官方给的对应的原码给它复制过来 */
cp .. /home/orangepi/wiringPiFromwindows/wiringOP-next/examples/serialTest.c .
/* LINUX里面一切皆文件每个硬件设备对应一个文件,查询串口文件 */
ls /dev/ttyS*
/*dev串口S0用来调试信息,当然S0串口禁用掉默认串口的系统消息,也可以当成通信串口来使用,网上有各种命令和配置方式,供大家各自去研究*/
/*dev串口S1这个板子是不支持的,dev串口S5用来信息交互*/
// 如何看orangepi支持S几,可使用gpio readall命令查看 TXD,RXD支持.几
/dev/ttyS0 /dev/ttyS1 /dev/ttyS2 /dev/ttyS3 /dev/ttyS4 /dev/ttyS5
2.4. 修改配置文件:
如果使用的为Linux5.16内核的系统,uart5默认是关闭的,需要手动打开才能使用。
在/boot/orangepiEnv.txt中加入uart5配置,保存退出然后输入指令reboot,重启 Linux系统就可以打开uart5。
orangepi@orangepizero2:~/waishe$ sudo vim /boot/orangepiEnv.txt
2.5. 基于wiringPi库的串口开发:
#include <stdio.h> // 包含标准输入输出库头文件
#include <string.h> // 包含字符串处理库头文件
#include <errno.h> // 包含错误号头文件
#include <wiringPi.h> // 包含wiringPi库头文件,用于GPIO控制
#include <wiringSerial.h> // 包含wiringSerial库头文件,用于串口通信
#include <stdlib.h> // 包含标准库头文件
#include <pthread.h> // 包含线程库头文件
#include <unistd.h> // 包含unistd库头文件,用于延时
int fd;
/* 发送线程函数 */
void* Sendhandler()
{
char *sendbuf = NULL;
sendbuf = (char *)malloc(32 * sizeof(char)); // 开辟内存空间
while(1){
memset(sendbuf, '\0', strlen(sendbuf)); // 清空缓冲区
scanf("%s",sendbuf); // 输入数据
while(*sendbuf != '\0'){ // 循环发送数据
serialPutchar(fd, *sendbuf++); // 发送数据
}
}
}
/* 接收线程函数 */
void* Recvhandler()
{
while(1){
while(serialDataAvail(fd)){ // 循环接收数据
printf("%c",serialGetchar(fd)); // 接收数据并打印
fflush(stdout); // 清空缓冲区
}
}
}
int main()
{
pthread_t idSend; // 定义线程发送id
pthread_t idRecv; // 定义线程接收id
fd = serialOpen("/dev/ttyS5", 115200); // 打开串口设备 "/dev/ttyS5",设置波特率为 115200
if(fd < 0){
printf("Error opening serial port\n"); // 输出错误信息
return -1;
}
pthread_create(&idSend, NULL, Sendhandler, NULL); // 创建发送线程
pthread_create(&idRecv, NULL, Recvhandler, NULL); // 创建接收线程
if (wiringPiSetup () == -1) // 初始化wiringPi
{
fprintf (stdout, "Unable to start wiringPi: %s\n", strerror (errno)) ; // 输出错误信息
return 1 ;
}
while(1){ // 循环发送数据
sleep(10); // 等待10秒
}
putchar('\n'); // 输出换行符
return 0;
}
这段代码的主要功能是通过串口进行双向通信。它使用两个线程分别处理发送和接收数据。发送线程从标准输入读取数据并通过串口发送,接收线程从串口读取数据并打印到标准输出。主函数负责初始化串口和wiringPi库,并创建发送和接收线程。
- fd = serialOpen("/dev/ttyS5", 115200);
fd 是一个文件描述符,表示成功打开的串口设备。
这个文件描述符在后续的串口通信中用来标识和访问该串口。
serialOpen("/dev/ttyS5", 115200) 是调用 wiringSerial 库中的 serialOpen 函数,其参数包括:
"/dev/ttyS5":指定要打开的串口设备。在 Linux 系统中,/dev/ttyS* 通常指代串口设备,
ttyS5 表示第六个串口(从 0 开始计数)。
115200:设置串口的波特率(数据传输速率),单位为比特每秒(bps)。
115200 是一个常用的波特率,适用于许多串口通信应用。
成功打开串口后,程序可以使用返回的文件描述符 fd 进行串口数据的发送和接收。
如果打开失败(例如设备不存在或权限问题),fd 会返回一个负值,程序会输出错误信息并终止。
- serialPutchar(fd, *sendbuf++);
serialPutchar(fd, *sendbuf++)
使用 wiringSerial 库中的 serialPutchar 函数,将字符发送到串口,其中:
fd 是之前打开串口时返回的文件描述符,表示要通过哪个串口进行通信。
*sendbuf 是当前指向的字符(即发送缓冲区中的第一个字符)。
sendbuf++ 在发送字符后,会将指针 sendbuf 向前移动一个位置,以指向下一个字符。
这个循环发送的过程会持续到字符串的末尾(即遇到字符 \0),实现逐字符地将消息发送到串口。
- serialDataAvail(fd)
serialDataAvail(fd) 是 wiringSerial 库中的一个函数,用于检查指定串口中是否有可用的数据。
具体来说:
fd 是打开串口时返回的文件描述符,指明要检查哪个串口。
该函数返回一个整数值,表示串口缓冲区中可读取的字节数。
如果返回值大于零,则表示有数据可以读取;如果返回值为零,则表示没有可用数据。
通过调用这个函数,接收线程可以在进入接收循环前检查是否有数据可读,从而避免空读操作,提高效率。
- printf("%c",serialGetchar(fd)); // 接收数据并打印
这行代码的作用是从串口接收一个字符并将其打印到标准输出。具体来说:
serialGetchar(fd) 是 wiringSerial 库中的一个函数,用于从指定的串口读取一个字符。
fd 是之前打开串口时返回的文件描述符,指示要从哪个串口读取数据。
printf("%c", ...) 将读取到的字符格式化为字符类型并输出到标准输出(通常是终端或控制台)。
因此,这行代码的作用是实时接收串口数据,并将每个接收到的字符直接显示出来,
允许用户看到从串口发送过来的信息。
这段代码的主要功能是通过串口进行双向通信。它使用两个线程分别处理发送和接收数据。发送线程从标准输入读取数据并通过串口发送,接收线程从串口读取数据并打印到标准输出。主函数负责初始化串口和wiringPi库,并创建发送和接收线程。
2.6. 不使用wiringPi库自己实现串口通信:
/*mySerial.h*/
//@Author: <NAME> 打开指定的串口设备,并设置波特率
int mySerialOpen(const char *device, const int baud);
/*
* @param device 串口设备名称,如"/dev/ttyUSB0"
* @param baud 波特率,如9600、115200等
* @return 成功返回文件描述符,失败返回-1
*/
// @Author: 向指定的串口设备发送字符串
void mySerialSendString(const int fd, const char *str);
/*
* @param fd 串口设备文件描述符
* @param str 要发送的字符串
* @return 无
*/
// @Author: 从指定的串口设备读取字符串
int mySerialReadString(const int fd, char *buffer);
/*
* @param fd 串口设备文件描述符
* @param buffer 读取到的字符串存放的缓冲区
* @return 读取到的字符串的长度,失败返回-1
*/
/*mySerialTool.c*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdarg.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
/*打开指定的串口设备,并设置波特率*/
int SerialOpen(const char *device, const int baud)
{
struct termios options; // 串口配置参数
speed_t myBaud; // 波特率
int status, fd; // 状态和文件描述符
//根据传入的波特率参数设置相应的波特率
switch(baud){
case 9600:
myBaud = B9600;
break;
case 115200:
myBaud = B115200;
break;
default:
printf("不支持的波特率!\n");
return -2;
}
//打开串口设备
if( (fd = open(device, O_RDWR | O_NOCTTY | O_NDELAY)) == -1){
printf("无法打开串口设备\n");
return -1;
}
// 设置文件描述符的标志为读写模式
fcntl(fd, F_SETFL, O_RDWR);
// 获取当前串口配置
tcgetattr(fd, &options);
// 设置串口为原始模式,无特殊处理
cfmakeraw(&options);
// 设置输入波特率
cfsetispeed(&options, myBaud);
// 设置输出波特率
cfsetospeed(&options, myBaud);
// 清除标志位并设置数据格式
options.c_cflag |= (CLOCAL | CREAD);
options.c_cflag &= ~PARENB; // 无奇偶校验位
options.c_cflag &= ~CSTOPB; // 1个停止位
options.c_cflag &= ~CSIZE; // 清除数据位
options.c_cflag |= CS8; // 设置为8位数据位
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // 非规范模式,不执行输入处理
options.c_oflag &= ~OPOST; // 输出处理
// 设置读取时的超时和最小接收字符数
options.c_cc[VMIN] = 0;
options.c_cc[VTIME] = 100; // 10秒超时
// 应用串口配置
tcsetattr(fd, TCSANOW, &options);
// 使用ioctl设置串口的DTR和RTS信号
ioctl(fd, TIOCMGET, &status);
status |= TIOCM_DTR;
status |= TIOCM_RTS;
ioctl(fd, TIOCMSET, &status);
// 短暂延时
usleep(10000); // 10毫秒延时
return fd; // 返回文件描述符
}
/*向指定的串口设备发送字符串*/
void SerialWriteString(const int fd, const char *str)
{
int ret;
ret = write(fd, str, strlen(str)); // 发送字符串
if(ret == -1){
printf("串口发送失败!\n");
exit(-1); // 发送失败,退出程序
}
}
/*从指定的串口设备读取字符串*/
int SerialReadString(const int fd, char *buffer)
{
int n_read;
n_read = read(fd, buffer, 32); // 读取串口数据
if(n_read == -1){
printf("串口读取失败!\n");
exit(-1); // 读取失败,退出程序
}
}
uartTool.h
int SerialOpen(const char *device, const int baud);
void SerialWriteString(const int fd, const char *str);
int SerialReadString(const int fd, char *buffer);
uart.c
/*mySerialTest.c*/
#include <stdio.h> // 包含标准输入输出库头文件
#include <string.h> // 包含字符串处理库头文件
#include <errno.h> // 包含错误号头文件
#include <stdlib.h> // 包含标准库头文件
#include <pthread.h> // 包含线程库头文件
#include <unistd.h> // 包含unistd库头文件,用于延时
#include "mySerial.h" // 包含自定义串口通信头文件
int fd; // 串口文件描述符
void* readSerial()
{
char buffer[32];
while(1){
memset(buffer, '\0', sizeof(buffer)); // 清空缓冲区
SerialReadString(fd, buffer); // 读取串口数据
printf("从串口中读取到的数据是:%s\n",buffer); // 打印读取到的数据
}
}
void* sendSerial()
{
char buffer[32] = {'\0'};
while(1){
memset(buffer, '\0', sizeof(buffer)); // 清空缓冲区
scanf("%s",buffer); // 输入要发送的数据
SerialWriteString(fd, buffer); // 发送数据到串口
}
}
int main(int argc, char **argv)
{
char deviceName[32] = {'\0'}; // 设备名
pthread_t readThread; // 读取线程
pthread_t sendThread; // 发送线程
if(argc < 2){
printf("Usage: %s /dev/ttyS?\n", argv[0]);
return -1;
}
strcpy(deviceName, argv[1]); // 拷贝设备名
if( (fd = SerialOpen(deviceName, 115200)) == -1){ // 打开串口
printf("open %s failed\n", deviceName);
}
pthread_create(&readThread, NULL, readSerial, NULL); // 创建读取线程
pthread_create(&sendThread, NULL, sendSerial, NULL); // 创建发送线程
while(1){
sleep(10); // 延时10秒
}
return 0;
}
好了。看到这里,我们orangepi基础入门就结束啦!书写不易。给个点赞,关注吧!!!