Flythings学习(四)串口通信

Flythings学习(四)串口通信APP 层在 ProtocolSend cpp 当 APP 层需要发送数据到 MCU 的时候直接调用 sendprotocol 函数 并在 sendProtocol 函数中实现

大家好,欢迎来到IT知识分享网。


1 串口编程基本步骤

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); //设置波特率为 newtio.c_cflag = B | 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; } 
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]; /* 控制字符特性*/ }; 

tcgetattr(fd, &oldtio); 这个函数的作用,是取得终端介质(fd)初始值,并把其值 赋给oldtio;函数可以从后台进程中调用;但是,终端属性可能被后来的前 台进程所改变。

tcflush是丢掉写入引用的对象,但是尚未传输的数据,或者收到但是尚未读取的数据

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 = B | 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中的串口通讯

uart协议解析和封装的串口HAL层:
uartContext:串口的实际控制层,提供串口的开关、收发接口
ProtocolData:定义通讯的数据结构体,用于保存通讯协议转化出来的实际变量
protocolSender:完成数据发送的封装
ProtocolParser:完成数据的协议解析部分,然后将解析号的数据放入ProtocolData中,同时管理了应用监听串口数据变化的回到接口
APP应用逻辑层:
通过ProtocolParser提供的接口注册串口数据接收监听获取串口更新出来的ProtocolData
通过ProtocolSender提供的接口往MCU发送指令信息






5 协议接收部分使用和修改方法

在这里插入图片描述

// 需要打印协议数据时,打开以下宏 //#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); 

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/128265.html

(0)
上一篇 2025-09-01 20:20
下一篇 2025-09-01 20:26

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信