大家好,欢迎来到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