文章目录
- 1 串口编程基本步骤
- 1.1 打开串口
- 1.2 配置串口
- 1.3 读串口
- 1.4 发送串口
- 1.5 关闭串口
- 2 综合使用
- 3 如何在软件上保证串口稳定通信
- 4 flythings中的串口通讯
- 5 协议接收部分使用和修改方法
- 6 通讯协议数据怎么和UI控件对接
1 串口编程基本步骤
串口通信有5个步骤
1.打开串口
2.配置串口
3.读串口
4.写串口
5.关闭串口
1.1 打开串口
#include <fcntl.h>
int fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY);
open是linux系统函数,open成功,返回0,失败返回1。/dev/ttys0可以理解为串口号,类似于windows系统上的com1,
1.2 配置串口
int openUart() {
int fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY);
struct termios oldtio = { 0 };
struct termios newtio = { 0 };
tcgetattr(fd, &oldtio);
//设置波特率为115200
newtio.c_cflag = B115200 | CS8 | CLOCAL | CREAD;
newtio.c_iflag = 0; // IGNPAR | ICRNL
newtio.c_oflag = 0;
newtio.c_lflag = 0; // ICANON
newtio.c_cc[VTIME] = 0;
newtio.c_cc[VMIN] = 1;
tcflush(fd, TCIOFLUSH);
tcsetattr(fd, TCSANOW, &newtio);
//设置为非阻塞模式,这个在读串口的时候会用到
fcntl(fd, F_SETFL, O_NONBLOCK);
return fd;
}
termios函数族提供了终端接口,用于控制非同步通信端口
他的结构体
struct termios
{
unsigned short c_iflag; /* 输入模式标志*/
unsigned short c_oflag; /* 输出模式标志*/
unsigned short c_cflag; /* 控制模式标志*/
unsigned short c_lflag; /*区域模式标志或本地模式标志或局部模式*/
unsigned char c_line; /*行控制line discipline */
unsigned char c_cc[NCC]; /* 控制字符特性*/
};
每个所对应的关系在下面这个博客
termios 相关知识https://blog.csdn.net/lizuobin2/article/details/51775277
tcgetattr(fd, &oldtio); 这个函数的作用,是取得终端介质(fd)初始值,并把其值 赋给oldtio;函数可以从后台进程中调用;但是,终端属性可能被后来的前 台进程所改变。
tcflush是丢掉写入引用的对象,但是尚未传输的数据,或者收到但是尚未读取的数据
tcsetattr(fd, TCSANOW, &newtio);
设置与终端相关的参数 (除非需要底层支持却无法满足),使用termios_p 引用的 termios 结构。optional_actions (tcsetattr函数的第二个参数)指定了什么时候改变会起作用,这里是改变立即发生
1.3 读串口
#include <fcntl.h>
unsigned char buffer[1024] = {0};
int ret = read(fd, buffer, sizeof(buffer));
1.4 发送串口
#include <fcntl.h>
unsigned char buffer[4] = {0};
buffer[0] = 0x01;
buffer[1] = 0x02;
buffer[2] = 0x03;
buffer[3] = 0x04;
int ret = write(fd, buffer, sizeof(buffer));
write可以发送串口数据
1.5 关闭串口
#include <fcntl.h>
close(fd);
2 综合使用
/*
* test.cpp
*
* Created on: 2024年10月14日
* Author: AA
*/
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc, char **argv){
// 创建串口
int fd = open("/dev/ttyS0",O_RDWR | O_NOCTTY);
if(fd < 0){
// 打开串口失败
return -1;
}
// 配置串口
struct tremios oldtio = {0};
struct termios newtio = {0};
tcgetattr(fd,oldtio);
newtio.c_cflag = B115200 | CS8 | CLOCAL | CREAD;
newtio.c_iflag = 0;
newtio.c_oflag = 0;
newtio.c_lflag = 0;
newtio.c_cc[VTIME] = 0;
newtio.c_cc[VMIN] = 1;
tcflush(fd, TCIOFLUSH);
tcsetattr(fd, TCSANOW, &newtio);
// 设置为非阻塞模式
fcntl(fd, F_SETFL, O_NONBLOCK);
// 监听和发送
while (true) {
unsigned char buffer[1024] = {0};
int ret = read(fd, buffer, sizeof(buffer));
if (ret > 0) {
//依次将读取到的数据输出到日志
for (int i = 0; i < ret; ++i) {
LOGD("收到%02x", buffer[i]);
}
//当收到数据时,再将收到的数据原样发送
int n = write(fd, buffer, ret);
if (n != ret) {
LOGD("发送失败");
}
//当收到0xFF时,跳出循环
if (buffer[0] == 0xFF) {
break;
}
} else {
//没收到数据时,休眠50ms,防止过度消耗cpu
usleep(1000 * 50);
}
}
close(fd);
return 0;
}
3 如何在软件上保证串口稳定通信
串口通信会制定通信协议,一般包括帧头、帧尾、帧内容、校验等部分。并且由于read函数不能保证一次性将当时串口收到的所有数据都返回,所以需要多次调用read函数,然后拼接起来
//提高buffer数组的作用域,使得while循环中不会清空数据
unsigned char buffer[1024] = {0};
// 增加一个`legacy`变量,表示buffer中遗留的数据长度
int legacy = 0;
while (true) {
//根据legacy的大小,调整缓冲区的起始指针及大小,防止数据覆盖
int ret = read(fd, buffer + legacy, sizeof(buffer) - legacy);
if (ret > 0) {
if ((buffer[0] == 0xFF) && (buffer[1] == 0x55)) {
if ((ret + legacy) == 10) {
LOGD("正确读到一帧数据");
//清空legacy
legacy = 0;
} else if (ret < 10) {
legacy += ret;
LOGD("协议头正确,但是帧长度不够,则暂存在buffer里");
}
}
//当收到数据时,再将收到的数据原样发送
int n = write(fd, buffer, ret);
if (n != ret) {
LOGD("发送失败");
}
//当收到0xFF时,跳出循环
if (buffer[0] == 0xFF) {
break;
}
} else {
//没收到数据时,休眠50ms,防止过度消耗cpu
usleep(1000 * 50);
}
}
用于处理串口通信的while循环应该单独开一个线程
4 flythings中的串口通讯
flythings自带一套串口通讯的框架
uart协议解析和封装的串口HAL层:
uartContext:串口的实际控制层,提供串口的开关、收发接口
ProtocolData:定义通讯的数据结构体,用于保存通讯协议转化出来的实际变量
protocolSender:完成数据发送的封装
ProtocolParser:完成数据的协议解析部分,然后将解析号的数据放入ProtocolData中,同时管理了应用监听串口数据变化的回到接口
APP应用逻辑层:
通过ProtocolParser提供的接口注册串口数据接收监听获取串口更新出来的ProtocolData
通过ProtocolSender提供的接口往MCU发送指令信息
这个流程大概是这样
具体流程
最终都是通过UartContext与MCU进行串口通信的,
5 协议接收部分使用和修改方法
通讯协议格式修改
CommDef.h 文件中定义了同步帧头信息及最小数据包大小信息:
// 需要打印协议数据时,打开以下宏
//#define DEBUG_PRO_DATA
// 支持checksum校验,打开以下宏
//#define PRO_SUPPORT_CHECK_SUM
/* SynchFrame CmdID DataLen Data CheckSum (可选) */
/* 2Byte 2Byte 1Byte N Byte 1Byte */
// 有CheckSum情况下最小长度: 2 + 2 + 1 + 1 = 6
// 无CheckSum情况下最小长度: 2 + 2 + 1 = 5
#ifdef PRO_SUPPORT_CHECK_SUM
#define DATA_PACKAGE_MIN_LEN 6
#else
#define DATA_PACKAGE_MIN_LEN 5
#endif
// 同步帧头
#define CMD_HEAD1 0xFF
#define CMD_HEAD2 0x55
在ProtocolParser文件中配置文件命令格式
/**
* 功能:解析协议
* 参数:pData 协议数据,len 数据长度
* pdata[0]、[1]、[2]、[3]都是协议头,[4]开始是数据
* 返回值:实际解析协议的长度
*/
int parseProtocol(const BYTE *pData, UINT len) {
UINT remainLen = len; // 剩余数据长度
UINT dataLen; // 数据包长度
UINT frameLen; // 帧长度
/**
* 以下部分需要根据协议格式进行相应的修改,解析出每一帧的数据
*/
while (remainLen >= DATA_PACKAGE_MIN_LEN) {
// 找到一帧数据的数据头
// pdata[0]如果不是协议头,说明是数据,往后继续找,直到找到协议头
while ((remainLen >= 2) && ((pData[0] != CMD_HEAD1) || (pData[1] != CMD_HEAD2))) {
pData++;
remainLen--;
continue;
}
// 如果剩下的长度小于最小的包长,则说明已经传完了,退出循环
if (remainLen < DATA_PACKAGE_MIN_LEN) {
break;
}
dataLen = pData[4];
frameLen = dataLen + DATA_PACKAGE_MIN_LEN;
if (frameLen > remainLen) {
// 数据内容不全
break;
}
// 打印一帧数据,需要时在CommDef.h文件中打开DEBUG_PRO_DATA宏
#ifdef DEBUG_PRO_DATA
for (int i = 0; i < frameLen; ++i) {
LOGD("%x ", pData[i]);
}
LOGD("\n");
#endif
// 支持checksum校验,需要时在CommDef.h文件中打开PRO_SUPPORT_CHECK_SUM宏
#ifdef PRO_SUPPORT_CHECK_SUM
// 检测校验码
if (getCheckSum(pData, frameLen - 1) == pData[frameLen - 1]) {
// 解析一帧数据
procParse(pData, frameLen);
} else {
LOGE("CheckSum error!!!!!!\n");
}
#else
// 解析一帧数据
procParse(pData, frameLen);
#endif
pData += frameLen;
remainLen -= frameLen;
}
return len - remainLen;
}
如果协议头需要更改
// 1.修改协议头部分的定义,如果协议头长度有变化,则要注意修改协议头判断部分语句。
#define CMD_HEAD1 0xFF
#define CMD_HEAD2 0x55
// 2.协议头长度变化的时候需要修改这里。
while ((mDataBufLen >= 2) && ((pData[0] != CMD_HEAD1) || (pData[1] != CMD_HEAD2)))
// 3.协议长度需要更改
// 这里的pData[4] 代表的是第5个数据是长度的字节,如果变化了在这里修改一下。
dataLen = pData[4];
// 帧长度一般是数据长度加上头尾长度。如果协议中传的长度计算方式发生变化修改这个部分。
frameLen = dataLen + DATA_PACKAGE_MIN_LEN;
6 通讯协议数据怎么和UI控件对接
使用procParse进行解析
/*
* 协议解析
* 输入参数:
* pData: 一帧数据的起始地址
* len: 帧数据的长度
*/
void procParse(const BYTE *pData, UINT len) {
/*
* 解析Cmd值获取数据赋值到sProtocolData结构体中
*/
switch (MAKEWORD(pData[2], pData[3])) {
case CMDID_POWER:
sProtocolData.power = pData[5];
LOGD("power status:%d",sProtocolData.power);
break;
}
notifyProtocolDataUpdate(sProtocolData);
}
通过notifyProtocolDataUpdate 将封装好的数据,发送给UI界面
sprotocolData可以增加数据变量
在UI的activity中,默认已经创建好了一个串口数据回调接口
static void onProtocolDataUpdate(const SProtocolData &data) {
// 串口数据回调接口
if (mProtocolData.power != data.power) {
mProtocolData.power = data.power;
}
if (mProtocolData.eRunMode != data.eRunMode) {
mProtocolData.eRunMode = data.eRunMode;
mbtn_autoPtr->setSelected(mProtocolData.eRunMode == E_RUN_MODE_MANUAL);
if (mProtocolData.eRunMode != E_RUN_MODE_MANUAL) {
mbtn_external_windPtr->setText(mProtocolData.externalWindSpeedLevel);
mbtn_internal_windPtr->setText(mProtocolData.internalWindSpeedLevel);
}
}
...
}
mProtocolData是UI界面默认的数据,在onUI_init的时候进行初始化,通过串口更新
APP层在ProtocolSender.cpp,当APP层需要发送数据到MCU的时候直接调用sendprotocol函数,并在sendProtocol函数中实现
/**
* 需要根据协议格式进行拼接,以下只是个模板
*/
bool sendProtocol(const UINT16 cmdID, const BYTE *pData, BYTE len) {
BYTE dataBuf[256];
dataBuf[0] = CMD_HEAD1;
dataBuf[1] = CMD_HEAD2; // 同步帧头
dataBuf[2] = HIBYTE(cmdID);
dataBuf[3] = LOBYTE(cmdID); // 命令字节
dataBuf[4] = len;
UINT frameLen = 5;
// 数据
for (int i = 0; i < len; ++i) {
dataBuf[frameLen] = pData[i];
frameLen++;
}
#ifdef PRO_SUPPORT_CHECK_SUM
// 校验码
dataBuf[frameLen] = getCheckSum(dataBuf, frameLen);
frameLen++;
#endif
return UARTCONTEXT->send(dataBuf, frameLen);
}
按下按键发送时
BYTE mode[] = { 0x01, 0x02, 0x03, 0x04 };
sendProtocol(0x01, mode, 4);