本文目录
- 前述
- 一、手册查看
- 二、命令行调试串口
- 1. 查看设备节点
- 2. 使用stty命令设置串口
- 3. 查看串口配置信息
- 4. 调试串口
- 三、代码编写
- 1. 常用API
- 2. 例程
- ●线程优化
- ●poll优化
- ●select优化(功能和poll一样)
前述
在开始实验前,请一定要检查测试好所需硬件是否使用正常,不然调试过程中出现的问题,会让你很烦恼。因为我在测试的时候发现一直接收不到消息,后面才知道ttl转usb的tx引脚坏了。所以确保硬件良好是我们后续测试的基本保障。
一、手册查看
我们前面讲到在Linux中一切皆是文件!无论是上一章节的GPIO,还是串口这边都同样如此。首先,我们就先来使用命令行基本调试一下我们的串口,来确保串口的可用性。无论是什么开发板,我们都需要先查看手册来确定开发板的串口io。这里以香橙派AIPro为例,手册中提醒我们串口0已经被系统使用,不能当作普通串口给用户使用。 所以用户可用串口只有两个。分别是UART7和UART2。
查看设备节点,如下所示。
二、命令行调试串口
1. 查看设备节点
设备节点都在 /dev
目录下。
2. 使用stty命令设置串口
常用配置:stty -F /dev/ttyAMA2 115200 cs8 -parenb -cstopb iutf8
3. 查看串口配置信息
命令:stty -F /dev/ttyAMA2 -a
4. 调试串口
我们使用TTL转USB接口连接香橙派与电脑,使用串口调试助手测试。注意:在串口助手中要设置为UTF8显示。
坑:在使用TTL连接时,一定要接地线!以保证电气基准电位!不要只接TX和RX!!
(1)香橙派发送数据
(2)香橙派接收数据
三、代码编写
终端设备属性结构体,我们在操作一些设备文件时,常常会用到下面这个结构体。在Unix系统中常用于控制终端的输入输出参数,比如波特率、字符大小、控制字符等。通过操作这个结构体,可以对终端的各种属性进行设置和获取。
头文件:#include<termios.h>
struct termios {
unsigned short c_iflag;//控制终端的输入方式,如是否启用回车、换行等
unsigned short c_oflag; //控制终端的输出方式,如是否启用回车、换行等。
unsigned short c_cflag; //控制终端的控制模式,如波特率、数据位数等。
unsigned short c_lflag;//控制终端的本地模式,如是否启用回显、是否启用信号等。
unsigned char c_line;//行(线)规程,指定终端的行规程,比如终端是终端设备还是伪终端设备。
unsigned char c_cc[NCC]; //控制字符数组,用于定义特殊控制字符的行为,比如终端中的删除、结束、停止等功能键的行为。
};
这些成员里都有很多配置参数的宏定义,我们只需要将成员与要配置参数的宏定义进行|=
(置1)或&=~
(置0)操作即可配置相应功能。我这里只先列举出来几个常用的功能。这里我将不再展示参数的宏定义,因为太多了,很多也用不到,有需要的自己去查看其他博客来学习。
●配置举例如下:
struct termios termios_p; //初始化结构体
termios_p.c_cflag |=CS8; //设置八位数据位。
termios_p.c_cflag &=~CSTOPB; //设置一位停止位。
termios_p.c_cflag &=~PARENB; //无奇偶校验位。
termios_p.c_lflag &=~ECHO ; //不回显
1. 常用API
(1)设置波特率。
波特率,常用 B2400,B4800,B9600,B115200,B460800。
int cfsetispeed(struct termios *termios_p,speed_t speed) //设置接收波特率
int cfsetospeed(struct termios *termios_p,speed_t speed) //设置发送波特率
//
(2)清空缓冲区数据。
主要用于清除输入和输出缓冲区中的数据。这个命令在处理串口通信时非常有用,特别是在初始化或重置通信通道时,以确保没有残留的数据干扰通信。
int tcflush(int fd,int queue_selector)
//int fd :文件描述符
//int queue_selector:
/* TCIFLUSH:清空正读的数据,且不会读出
TCOFLUSH:清空正写入的数据,且不会发送到终端
TCIOFLUSH:清空所有正在发生的 I/O 数据.
*/
(3)获取终端设备参数。
函数的作用是获取指定文件描述符(fd)所关联的终端设备的当前属性,并将这些属性存储到指定的 termios 结构体(termios_p)中。
int tcgetattr(int fd,struct termios *termios_p)
//int fd :文件描述符。
//struct termios *termios_p: 设备终端结构体。
(4)设置终端设备参数,激活配置。
int tcsetattr(int fd,int optional_actions,cons struct termios *termios_p)
//int fd :文件描述符。
//int optional_actions :
/* TCSANOW:不等数据传输完毕,立即改变属性
TCSADRAIN:等所有数据传输完毕,再改变属性
TCSAFLUSH:清空输入输出缓冲区才改变属性
*/
//cons struct termios *termios_p :终端设备属性的结构体。
2. 例程
uart.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <string.h>
#include "uart.h"
#include <termios.h>
void UART_Close(int fd)
{
close(fd);
}
int UART_Send(int fd, char *data)
{
int num;
num=write(fd, data, strlen(data));
return num;
}
int UART_Receive(int fd, char *receive)
{
int num;
num=read(fd, receive, sizeof(receive)-1);
return num;
}
int UART_Init(char *device, int baud)
{
int fd;
int ret;
struct termios termios_p;
//1.打开串口设备。不当作控制台。
fd = open(device, O_NOCTTY | O_RDWR);
if (fd < 0) {
perror("open error");
return -1;
}
//2. 填充设备结构体
memset(&termios_p, 0 ,sizeof(termios_p));
termios_p.c_cflag |= CREAD; //使能接收器的接收功能!必须配置的。
termios_p.c_cflag |= CLOCAL; //忽略调制解调器线路状态。
termios_p.c_cflag |=CS8; //设置八位数据位。
//这里为了方便观看,所以写出来下面的配置,这里其实可以不写,因为我们已经先前清空结构体了。
termios_p.c_cflag &=~CSTOPB; //设置一位停止位。
termios_p.c_cflag &=~PARENB; //无奇偶校验位。
termios_p.c_lflag &=~ECHO ; //不回显
// 设置超时和最小读取字符数。必须配置!!
termios_p.c_cc[VTIME] = 1;
termios_p.c_cc[VMIN] = 128;
switch (baud){ //设置波特率
case 9600:
cfsetispeed(&termios_p,B9600); //设置接收波特率
cfsetospeed(&termios_p,B9600); //设置发送波特率
break;
case 115200:
cfsetispeed(&termios_p,B115200); //设置接收波特率
cfsetospeed(&termios_p,B115200); //设置发送波特率
break;
default:
printf("不支持此波特率\n");
break;
}
//3. 清空接收/发送缓冲,准备发送和接收数据
tcflush(fd, TCIOFLUSH);
//4. 将配置好的设备结构体设置上(绑定),激活设置的配置。
ret =tcsetattr( fd,TCSAFLUSH,&termios_p);
if (ret < 0) {
perror("tcsetattr error");
return -3;
}
return fd; //返回文件描述符
}
uart.h
#ifndef __UART_H
#define __UART_H
void UART_Close(int fd);
int UART_Init(char *device, int baud);
int UART_Receive(int fd, char *receive);
int UART_Send(int fd, char *data);
#endif
main.c
#include <stdio.h>
#include <stdlib.h>
#include "uart.h"
#include <unistd.h>
#include <termios.h>
#include <string.h>
int main()
{
char buff[100];
int fd;
fd=UART_Init("/dev/ttyAMA2",115200);
if(fd<0){
perror("UART_Init error");
return -1;
}
while(1){
fgets(buff,sizeof(buff),stdin);
UART_Send(fd, buff);
}
UART_Close(fd);
return 0;
}
●线程优化
功能:添加线程实现可收可发!
main.c
#include <stdio.h>
#include <stdlib.h>
#include "uart.h"
#include <unistd.h>
#include <termios.h>
#include <string.h>
#include <pthread.h>
#include <signal.h>
pthread_attr_t attr; //线程属性
int fd; //文件描述符
void signal_task(int arg)
{
printf("销毁相关属性\n");
pthread_attr_destroy(&attr); //销毁线程属性
UART_Close(fd);
exit(0);
}
// 线程任务函数
void *task(void *arg) {
int fd = *(int *)arg;
int ret;
char buff[64];
while (1) {
memset(buff, 0, sizeof(buff));
ret = UART_Receive(fd, buff); // 读取数据到buff中
if (ret > 0) {
buff[ret] = '\0'; // 确保字符串以NULL结尾
printf("Receive: %s\n", buff);
}
else break;
}
pthread_exit(NULL);
}
int main()
{
char buff[100];
int ret;
pthread_t thread;
fd=UART_Init("/dev/ttyAMA2",115200);
if(fd<0){
perror("UART_Init error");
return -1;
}
signal(2, signal_task);
pthread_attr_init(&attr); //初始化线程属性
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); //设置子线程分离属性,子线程推出后自动销毁。
ret=pthread_create(&thread,NULL,task,(void *)&fd); //只有一个子线程,可以取地址操作。
if(ret<0){
perror("pthread_create error");
return -1;
}
while(1){
memset(buff,0,sizeof(buff));
fgets(buff,sizeof(buff),stdin);
UART_Send(fd, buff);
}
return 0;
}
●poll优化
当没有事件要读写时,释放CPU。当有事件发生时,执行相应动作。
uart.c
#include <poll.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <string.h>
#include <termios.h>
#include "uart.h"
extern struct pollfd fds;
void UART_Close(int fd)
{
close(fd);
}
int UART_Send(int fd, char *data)
{
int num;
int ret;
ret = poll(&fds, 1, 0);
if (ret == -1) {
perror("poll");
return -1;
}
else if (ret > 0) {
if(fds.revents &POLLOUT){
num=write(fd, data, strlen(data));
return num;
}
}
}
int UART_Receive(int fd, char *receive)
{
int num;
int ret;
ret = poll(&fds, 1, 0);
if (ret == -1) {
perror("poll");
return -1;
}
else if (ret > 0) {
if(fds.revents &POLLIN){
num=read(fd, receive, sizeof(receive)-1);
return num;
}
}
}
int UART_Init(char *device, int baud)
{
int fd;
int ret;
struct termios termios_p;
//1.打开串口设备。不当作控制台。
fd = open(device, O_NOCTTY | O_RDWR);
if (fd < 0) {
perror("open error");
return -1;
}
//2. 填充设备结构体
memset(&termios_p, 0 ,sizeof(termios_p));
termios_p.c_cflag |= CREAD; //使能接收器的接收功能!必须配置的。
termios_p.c_cflag |= CLOCAL; //忽略调制解调器线路状态。
termios_p.c_cflag |=CS8; //设置八位数据位。
//这里为了方便观看,所以写出来下面的配置,这里其实可以不写,因为我们已经先前清空结构体了。
termios_p.c_cflag &=~CSTOPB; //设置一位停止位。
termios_p.c_cflag &=~PARENB; //无奇偶校验位。
termios_p.c_lflag &=~ECHO ; //不回显
// 设置超时和最小读取字符数。必须配置!!
termios_p.c_cc[VTIME] = 1;
termios_p.c_cc[VMIN] = 128;
switch (baud){ //设置波特率
case 9600:
cfsetispeed(&termios_p,B9600); //设置接收波特率
cfsetospeed(&termios_p,B9600); //设置发送波特率
break;
case 115200:
cfsetispeed(&termios_p,B115200); //设置接收波特率
cfsetospeed(&termios_p,B115200); //设置发送波特率
break;
default:
printf("不支持此波特率\n");
break;
}
//3. 清空接收/发送缓冲,准备发送和接收数据
tcflush(fd, TCIOFLUSH);
//4. 将配置好的设备结构体设置上(绑定),激活设置的配置。
ret =tcsetattr( fd,TCSAFLUSH,&termios_p);
if (ret < 0) {
perror("tcsetattr error");
return -3;
}
return fd; //返回文件描述符
}
main.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <termios.h>
#include <string.h>
#include <pthread.h>
#include <signal.h>
#include <poll.h>
#include "uart.h"
pthread_attr_t attr; //线程属性
int fd; //文件描述符
struct pollfd fds;
void signal_task(int arg)
{
printf("销毁相关属性\n");
UART_Close(fd);
pthread_attr_destroy(&attr); //销毁线程属性
exit(0);
}
// 线程任务函数
void *task(void *arg) {
int ret;
char buff[64];
while (1) {
memset(buff, 0, sizeof(buff));
ret = UART_Receive(fds.fd, buff); // 读取数据到buff中
if (ret > 0) {
buff[ret] = '\0'; // 确保字符串以NULL结尾
printf("Receive: %s\n", buff);
}
else if(ret <0) break;
}
pthread_exit(NULL);
}
int main()
{
char buff[100];
int ret;
pthread_t thread;
char poll_state[64];
fd=UART_Init("/dev/ttyAMA2",115200);
if(fd<0){
perror("UART_Init error");
return -1;
}
fds.fd=fd;
fds.events = POLLOUT|POLLIN; // 有普通事件写/有普通事件读
signal(2, signal_task);
pthread_attr_init(&attr); //初始化线程属性
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); //设置子线程分离属性,子线程推出后自动销毁。
ret=pthread_create(&thread,NULL, task, NULL); //只有一个子线程,可以取地址操作。
if(ret<0){
perror("pthread_create error");
return -1;
}
while(1){
memset(buff,0,sizeof(buff));
fgets(buff,sizeof(buff),stdin);
UART_Send(fds.fd, buff);
}
return 0;
}
●select优化(功能和poll一样)
select 函数可以监视多个文件描述符,等待其中的一个或多个变为“就绪”状态。其功能和poll一样。
使用步骤:先定义一个结构体(检测读/ 检测写),将检测的文件描述符添加到这个结构体中。然后使用select进行绑定检测,如果检测到有事件发生,则函数返回值大于0,然后再进入大于0的函数体判断文件描述符在哪个结构体中(读结构体还是写结构体),如果文件描述符在写的结构体中,就进行写操作。反之相同。
返回值:成功时,返回已就绪的文件描述符数量。超时且没有文件描述符就绪时,返回0。出错时,返回-1,并设置 errno。
<1> 函数原型
#include <sys/select.h>
#include <sys/time.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
/*
nfds:指定要监视的文件描述符的数量。其值应为所有文件描述符中最大值加1。
readfds:指向 fd_set 结构的指针,表示需要监视的可读文件描述符集合。
writefds:指向 fd_set 结构的指针,表示需要监视的可写文件描述符集合。
exceptfds:指向 fd_set 结构的指针,表示需要监视的异常文件描述符集合。
timeout:指向 timeval 结构的指针,指定等待的超时时间。如果为 NULL,select 将无限等待。
*/
<2> 相关宏函数
FD_ZERO(fd_set *set); // fd_set 结构清零。
FD_SET(int fd, fd_set *set); //在 fd_set 结构中添加一个文件描述符。
FD_CLR(int fd, fd_set *set); //从 fd_set 结构中删除一个文件描述符。
FD_ISSET(int fd, fd_set *set); //检查文件描述符是否在 fd_set 结构中。
<3> 优化示例
uart.c
#include <sys/select.h>
#include <sys/time.h>
int UART_Send(int fd, char *data)
{
int num;
int ret;
fd_set writefds;
struct timeval timeout;
// 设置超时时间100ms
timeout.tv_sec = 0;
timeout.tv_usec = 100000;
FD_ZERO(&writefds); //清空结构体,丢弃原来旧的数据。
FD_SET(fd, &writefds); //在 fd_set 结构中添加一个文件描述符。
ret=select(fd+1, NULL, &writefds , NULL, &timeout);
if (ret == -1) {
perror("select");
return -1;
}
else if (ret > 0) {
if(FD_ISSET(fd,&writefds)){
num=write(fd, data, strlen(data));
return num;
}
}
}
int UART_Receive(int fd, char *receive)
{
int num;
int ret;
fd_set readfds;
struct timeval timeout;
// 设置超时时间100ms
timeout.tv_sec = 0;
timeout.tv_usec = 100000;
FD_ZERO(&readfds); //清空结构体,丢弃原来旧的数据。
FD_SET(fd, &readfds); //在 fd_set 结构中添加一个文件描述符。
ret=select(fd+1, &readfds, NULL, NULL, &timeout);
if (ret == -1) {
perror("poll");
return -1;
}
else if (ret > 0) {
if(FD_ISSET(fd, &readfds)){
num=read(fd, receive, sizeof(receive)-1);
return num;
}
}
}