前言
本篇以STM32F407VET6主控为基础进行论述。
文章的目的在于记录和引导,传递一些编写通信类功能会用到的基本思想,以及组合这些功能的思想。
匿名上位机V7版本的功能在本篇没有全部写出来,而是针对本上位机常用的功能举例来描述。
相信读者通过仔细阅读笔者的思想引述和具体代码实现能够触类旁通。
限于笔者水平有限,文章中的一些错误还望批评指正。
一、匿名上位机的通信帧格式
其中DATA 数据内容中的数据,采用小端模式,低字节在前,高字节在后。
从图中可以一目了然,一个通信帧共有7个部分,每个部分都有规定。这直接影响了后续结构体的定义,及其成员的使用。
每个部分的内存基本单元为字节。
二、简单面向对象封装
1.封装
按照(一)中的通信帧结构定义结构体:
/****参数对象结构体****/
typedef struct
{
uint16_t par_id; //<参数id
int32_t par_val; //<参数值
}par_struct;
/****通信帧对象结构体****/
typedef struct
{
uint8_t head; //<帧头
uint8_t target_addr; //<目标地址
uint8_t function_id; //<该帧要实现某功能的功能码id
uint8_t data_len; //<数据长度
uint8_t data[40]; //<数据内容,协议最高只支持40字节数据
uint8_t sum_check; //<和校验
uint8_t add_check; //<附加校验
par_struct* parameter; //<参数
}ano_frameStruct;
通过结构体的“打包”,我们将一个通信帧当作一个对象来进行操作。
为什么data定义大小是40?
协议中有不同类型的帧,每个类型的帧里面,数据所需的长度都是不一样的,有一些类型的帧数据长度固定,有一些类型的帧可以自主选择帧数据的长度。
其中灵活数据帧是协议中最自由的协议,它的数据长度最大允许为40字节,仔细看文档可知,这同时也是所有类型帧中支持数据长度最长的,这直接决定了我们data部分数组定义的大小:
2.接口化
应用于匿名上位机的接口化非常简单,就是在编写过程函数的时候,函数的形参,都以通信帧结构体指针作为输入,这样就能实现对通信帧对象的统一操作。
比如:
/*共同点:形参都含ano_frameStruct*类型指针*/
void ano_frame_reset(ano_frameStruct* frame);
void ano_check_calculate(ano_frameStruct* frame);
uint8_t ano_check(ano_frameStruct* frame);
void frame_turn_to_array(ano_frameStruct* frame,uint8_t*str);
//.....
//函数在后面会详细展开描述
笔者简单地举例了几个函数的定义形式,这样定义,就能实现对通信帧对象在某个操作中的通用化,比如复位操作,校验操作,通信帧结构体转连续数组操作等等。同时,这也是API那般高度封装的思想雏形。
三、编写上位机的基本功能(除接收逻辑)
1.部分宏定义
①硬件地址宏定义:
//目标地址宏定义
#define FRAME_HEADER 0XAA //<匿名协议固定帧头
#define GENERAL_OUTPUT_ADDR 0XFF //<广播型输出
#define HOST_ADDR 0XAF //<向上位机输出
#define PRO_ADDR 0X05 //<拓空者PRO飞控
#define SHUCHUAN_ADDR 0X10 //<匿名数传
#define GUANGLIU_ADDR 0X22 //<匿名光流
#define UWB_ADDR 0X30 //<匿名UWB
#define IMU_ADDR 0X60 //<匿名凌霄IMU
#define LINGXIAO_ADDR 0X61 //<匿名凌霄飞控
②参数ID宏定义
这里总计有166个参数id对,用户根据需要去进行宏定义即可。
因为不作飞控用途,笔者主要使用5~10:
对于序号1,有特殊用途,可以把自己的设备伪装成硬件地址中的各种飞控,这样就能使用这166个参数的交互了。否则是无法使用参数通信功能的!
#define HWTYPE 0X01 //<用于存储下位机的类型信息
#define ID_INFO5 0X05 //<预留位
#define ID_INFO6 0X06 //<预留位
#define ID_INFO7 0X07 //<预留位
#define ID_INFO8 0X08 //<预留位
#define ID_INFO9 0X09 //<预留位
#define ID_INFO10 0X0A //<预留位
③字符输出,有三个指定颜色
#define ANO_BLACK 0X00 //<字符黑色打印
#define ANO_RED 0X01 //<字符红色打印
#define ANO_GREEN 0X02 //<字符绿色打印
2.对象初始化与复位
#include <string.h>
static par_struct send_parameter; //<发送帧中的参数;
static par_struct rec_parameter; //<接收帧的参数;
static ano_frameStruct send_frame_struct; //<(发送)通信帧结构体
__IO ano_frameStruct rec_frame_struct; //<(接收)通信帧结构体,因不止在本.c文件使用,故不用static修饰
/**
* @brief 初始化通信帧结构体,使用前必须调用
* @param 无输入参数
* @retval 无返回
**/
void ano_frame_init(void)
{
/*参数结构体初始化*/
send_frame_struct.parameter=&send_parameter;
rec_frame_struct.parameter=&rec_parameter;
send_frame_struct.parameter->par_id=0;
send_frame_struct.parameter->par_val=0;
rec_frame_struct.parameter->par_id=0;
rec_frame_struct.parameter->par_val=0;
send_frame_struct.head=rec_frame_struct.head=FRAME_HEADER;//帧头固定是0XAA
send_frame_struct.target_addr=rec_frame_struct.target_addr=HOST_ADDR;
send_frame_struct.function_id=0XFF;//<协议中没有定义的功能ID,这样初始化目的是为了启动瞬间不做任何动作
memset(send_frame_struct.data,0,40);//<缓存默认全部置0
memset(rec_frame_struct.data,0,40);
}
/**
* @brief 复位通信帧结构体,ano_frame_init()必须要运行过一次
* @param 通信帧结构体对象
* @retval 无返回
**/
void ano_frame_reset(ano_frameStruct* frame)
{
frame->function_id=0XFF;
frame->data_len=0;
memset(frame->data,0,40);
frame->add_check=0;
frame->sum_check=0;
}
/**
* @brief 通信帧中参数结构体内成员的配置
* @param 通信帧结构体对象,参数ID与参数值
* @retval
**/
void ano_par_struct_config(ano_frameStruct* frame,uint16_t id,int32_t val)
{
frame->parameter->par_id=id;
frame->parameter->par_val=val;
}
3.数据校验的逻辑
/**
* @brief 通信帧校验计算
* @param 通信帧结构体对象
* @retval 无返回值
**/
static void ano_check_calculate(ano_frameStruct* frame)
{
frame->sum_check=0;
frame->add_check=0;
//除去和校验,附加校验及数据部分,有4个部分4个字节,长度固定
for(uint32_t i=0;i<4;i++)
{
frame->sum_check+= *(uint8_t*)(&frame->head+i);
frame->add_check+=frame->sum_check;
}
//获取数据长度部位,把数据部分全加上
for(uint32_t i=0;i<frame->data_len;i++)
{
frame->sum_check+=*((uint8_t*)(frame->data)+i);
frame->add_check+=frame->sum_check;
}
}
/**
* @brief 通信帧校验检查(接收上位机通信帧时用)
* @param 通信帧结构体对象
* @retval 1:校验成功 0:校验失败
**/
static uint8_t ano_check(ano_frameStruct* frame)
{
uint8_t sum_check=0;
uint8_t add_check=0;
for(uint32_t i=0;i<4;i++)
{
sum_check+= *(uint8_t*)(&frame->head+i);
add_check+=sum_check;
}
for(uint32_t i=0;i<frame->data_len;i++)
{
sum_check+=*((uint8_t*)(frame->data)+i);
add_check+=sum_check;
}
//如果计算与获取的相等,校验成功
if((sum_check==frame->sum_check)&&(add_check==frame->add_check))
return 1;
else
return 0;
}
4.串口UART的接口函数
作为STM32的UART外设接口,根据自己使用的串口修改就好。
/**
* @brief 匿名串口发送
* @param 字符串,数据字节个数
* @retval
*/
static void ano_usart_send(uint8_t*str,uint16_t num)
{
uint16_t cnt=0;
do
{
HAL_UART_Transmit(&huart1,((uint8_t*)(str))+cnt,1,1000);
cnt++;
}while(cnt<=num);
}
5.数据的处理
上位机只支持整形数据的通信。
对于数据部分,我们可能会发送8位的参数,16位的参数或32位的参数。
但是数据部分要求我们一个字节一个字节从低到高发送,所以我们需要对待传输的数据进行由低位到高位的字节截断。
//32位数据进行四个字节剥离拆分,从低位到高位
#define BYTE0(temp) (*(char*)(&temp))
#define BYTE1(temp) (*((char*)(&temp)+1))
#define BYTE2(temp) (*((char*)(&temp)+2))
#define BYTE3(temp) (*((char*)(&temp)+3))
如果一个通信帧的内容全部定下来了,我们需要把通信帧转为从低位到高位的线性数组:
/**
* @brief 通信帧结构体转化为线性数组
* @param 要转换的通信帧,缓存数组
* @retval
**/
static void frame_turn_to_array(ano_frameStruct* frame,uint8_t*str)
{
memcpy(str,(uint8_t*)frame,4);
memcpy(str+4,(uint8_t*)frame->data,frame->data_len);
memcpy(str+4+frame->data_len,(uint8_t*)(&frame->sum_check),2);
}
6.向上位机发送字符串
/**
* @brief 向上位机发送ASCII字符串
* @param color:希望上位机显示的字符串颜色,str:要发送的字符串
* @retval 无返回值
*/
void ano_send_string(uint8_t color,char* str)
{
uint8_t i=0,cnt=0;
uint8_t buff[46];
memset(send_frame_struct.data,0,40);
send_frame_struct.function_id=0XA0; //<信息输出--字符串(功能码0XA0)
send_frame_struct.data[cnt++]=color; //<选择上位机打印的颜色
/*字符串数据直接存入数据部分*/
while(*(str+i)!='\0')
{
send_frame_struct.data[cnt++]=*(str+i++);
if(cnt>40) //<若字符串长度超过40,强制结束
break;
}
send_frame_struct.data_len=cnt; //<记录下数据部分长度
ano_check_calculate(&send_frame_struct); //<计算校验和
frame_turn_to_array(&send_frame_struct,buff); //<通信帧转线性数组
ano_usart_send(buff,6+send_frame_struct.data_len);
}
/**
* @brief 向上位机发送ASCII字符串+数字组合
* @param value:32位的数值,str:要发送的字符串
* @retval
*/
void ano_send_message(char* str,int32_t value)
{
uint8_t i=0,cnt=0;
uint8_t buff[46];
memset(send_frame_struct.data,0,40);
send_frame_struct.function_id=0XA1; //信息输出--字符串+数字
/*协议规定VAL在前,先对要求的32位数据进行截断*/
send_frame_struct.data[cnt++]=BYTE0(value);
send_frame_struct.data[cnt++]=BYTE1(value);
send_frame_struct.data[cnt++]=BYTE2(value);
send_frame_struct.data[cnt++]=BYTE3(value);
/*再轮到字符串数据*/
while(*(str+i)!='\0')
{
send_frame_struct.data[cnt++]=*(str+i++);
if(cnt>40)
break;
}
send_frame_struct.data_len=cnt; //<记录下数据部分长度
ano_check_calculate(&send_frame_struct); //<计算校验和
frame_turn_to_array(&send_frame_struct,buff); //<通信帧转线性数组
ano_usart_send(buff,6+send_frame_struct.data_len);
}
7.向上位机发送灵活数据帧
/**
* @brief 发送灵活格式帧
* @param id:0xF1~0XFA,x_coordinate:x轴坐标值 ,y_coordinate:y轴坐标值
* !!!要传多少个参数完全可以自己进行计算,最高只支持40字节的数据,低位先输出
* 一般10个以内够用,40个字节限制,一个32位数据占4个字节,可以发送10个
* @retval
*/
void ano_send_flexible_frame(uint8_t id,int32_t x_coordinate,int32_t y_coordinate)
{
uint8_t buff[46];
memset(send_frame_struct.data,0,40);
send_frame_struct.function_id=id;
send_frame_struct.data_len=8; //<根据自己的参数数填写
/*第一个x_coordinate数据从低位到高位截断*/
send_frame_struct.data[0]=BYTE0(x_coordinate);
send_frame_struct.data[1]=BYTE1(x_coordinate);
send_frame_struct.data[2]=BYTE2(x_coordinate);
send_frame_struct.data[3]=BYTE3(x_coordinate);
/*第二个数据y_coordinate从低位到高位截断*/
send_frame_struct.data[4]=BYTE0(y_coordinate);
send_frame_struct.data[5]=BYTE1(y_coordinate);
send_frame_struct.data[6]=BYTE2(y_coordinate);
send_frame_struct.data[7]=BYTE3(y_coordinate);
/*第N个数据xxx从低位到高位截断*/
//......用户自行添加
ano_check_calculate(&send_frame_struct);
frame_turn_to_array(&send_frame_struct,buff);
ano_usart_send(buff,6+send_frame_struct.data_len);
}
效果图:
四、有限状态机FSM
有限状态机,顾名思义,这类状态机的状态数量是有限的,在不同阶段会呈现不同的运行状态,并且不重复。如果设计的系统使用了有限状态机的方法,那么在某一个时刻,它必定是处于所有状态中的其中一个状态。
1.有限状态机的基本要素
①状态。一个状态机,必定有多个状态。
②条件。进入一个状态后,要判断一些条件,看看已有的条件,满不满足现在所处的状态的特征,或,要做什么动作,或决定从现状态要迁移到哪个状态。
③动作,在当前状态,要执行什么操作,同时迁移也是一种动作。
④迁移,一个状态迁移到另一个状态。
2.一个简单的状态机
①状态分别有S0,S1,S2,S3,S4五个状态,其中S0是初始状态
②条件。每到一个状态,bool初始化为-1,当外部事件或中断发生,把bool变成0或1时,这是我们进行迁移的条件。
③动作。主要是对条件进行判断和迁移两个动作
④迁移,满足条件就迁移。若外部事件或中断没有到来,bool始终是-1,将不断停留在现在的状态。
3.匿名上位机与MCU的交互
4.STM32串口的接收机制
对于STM32串口接收,一次接收,只能接收一个字节,但是,我们匿名上位机的通信帧,最多达46字节,才算一次完整的通信帧接收。基于硬件机制与协议内容,我们需要在接收函数中,建立一个状态机。
本例示范中断接收方式:
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
static uint8_t data=0;
/* USER CODE END USART1_IRQn 0 */
// HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_ORE)!=RESET)
{
data=huart1.Instance->DR;
__HAL_UART_CLEAR_OREFLAG(&huart1);//<开启错误中断,目的是防止波特率过高造成的错误
}
/*接收逻辑*/
if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_RXNE)!=RESET)
{
data=huart1.Instance->DR;
ano_read_one_byte(data);//<状态机函数,实现数据接收与状态机
__HAL_UART_CLEAR_FLAG(&huart1,UART_FLAG_RXNE);
}
/* USER CODE END USART1_IRQn 1 */
}
5.接收状态分析
五个状态:正在接收帧头,正在接收帧目标地址,正在接收帧功能码ID,正在接收帧数据部分长度信息,正在接收帧数据部分,正在接收帧的和校验,正在接收帧的附加校验
简单描述一下接收过程:
① 初始状态(它的选定很重要): 等待接收帧头,如果接收到的不是0XAA,认为不是上位机发过来的数据,停留于该阶段,如 绿色自返箭头所示。
②接收目标地址:接收的地址必定是硬件地址那8种,可以选择性根据这一特征进行判断,来决定是否迁移;笔者这里没有做判断,接收完地址,直接迁移到下一个状态。
③接收功能码ID:必定是0XE0,0XE1或0XE2其一,若不是,通信帧发生错误,迁移到初始状态;若是,迁移到下一步。
④接收数据部分长度信息:必定是小于或等于40,如果小于0或大于40,通信出错,迁移到初始状态;若是,迁移到下一步。
⑤接收数据:在到达数据长度前,都 自返于此状态接收数据,接收完之后呢,就迁移到下一个状态;
⑥接收和校验
⑦ 接收附加校验: 到这时候,完整的一帧,已经接收完了 ,执行反馈操作,初始化通信帧,等待下一个接收到来。
五、状态机思想应用于接收逻辑
1.头文件增加类型定义
//数据帧的7个不同状态
enum FRAME_PART
{
HEAD_PART=0, //<正在接收帧头
ADDR_PART, //<正在接收帧目标地址
ID_PART, //<正在接收帧功能码ID
DATA_LEN_PART, //<正在接收帧数据部分长度信息
DATA_PART, //<正在接收帧数据部分
SC_PART, //<正在接收帧的和校验
AC_PART //<正在接收帧的附加校验
};
2.参数读写类帧的通信
①反馈给上位机的校验帧
/**
* @brief 发送数据校验帧
* @param id_get:接收到的通信帧功能id,sc_get:接收到的通信帧校验,ac_get:接收到的通信帧附加校验
* @retval
*/
static void ano_send_check_frame(uint8_t id_get,uint8_t sc_get,uint8_t ac_get)
{
uint8_t buff[46];
memset(send_frame_struct.data,0,40);
send_frame_struct.function_id=0X00;
send_frame_struct.data_len=3; //这里要随传递的参数自行计算,最好不要用sizeof,会减慢通信速度
send_frame_struct.data[0]=id_get;
send_frame_struct.data[1]=sc_get;
send_frame_struct.data[2]=ac_get;
ano_check_calculate(&send_frame_struct);
frame_turn_to_array(&send_frame_struct,buff);
ano_usart_send(buff,6+send_frame_struct.data_len);
}
②反馈给上位机的参数读取返回帧
/**
* @brief 参数读取返回
* @param 发送通信帧结构体对象,上位机要读取的值
* @retval 无返回
*/
void ano_send_parameter_frame(ano_frameStruct* send_frame,int32_t val)
{
uint8_t buff[46];
memset(send_frame_struct.data,0,40);
send_frame_struct.function_id=0XE2;//参数写入反馈
send_frame_struct.data_len=6;
/*这里固定*/
send_frame_struct.data[0]=BYTE0(send_frame->parameter->par_id);
send_frame_struct.data[1]=BYTE1(send_frame->parameter->par_id);
send_frame_struct.data[2]=BYTE0(val);
send_frame_struct.data[3]=BYTE1(val);
send_frame_struct.data[4]=BYTE2(val);
send_frame_struct.data[5]=BYTE3(val);
ano_check_calculate(&send_frame_struct);
frame_turn_to_array(&send_frame_struct,buff);
ano_usart_send(buff,6+send_frame_struct.data_len);
}
③状态机
/**
* @brief 接收上位机的数据,根据所处的不同状态做不同的动作
* @param 串口收到的单字节数据
* @retval 无
*/
void ano_read_one_byte(uint8_t data)
{
static uint8_t status=HEAD_PART; //<初始状态,默认等待接收帧头
static uint8_t cnt=0;
switch (status)
{
case HEAD_PART:
{
if(data==0XAA) //<判断,确保HEAD_PART状态收到的就是固定帧头0XAA,否则一直停留于该状态
{
status=ADDR_PART;
//<复位接收通信帧,因为接下来的内容都来源于接收中断
ano_frame_reset(&rec_frame_struct);
}
break;
}
case ADDR_PART:
{
rec_frame_struct.target_addr=data;
status=ID_PART;
break;
}
case ID_PART:
{
rec_frame_struct.function_id=data;
//<匿名上位机只有在0XE0,0XE1或0XE2的功能命令中需要MCU反馈,否则就是接收出现了错误
if((rec_frame_struct.function_id==0XE0)||(rec_frame_struct.function_id==0XE1)||(rec_frame_struct.function_id==0XE2))
status=DATA_LEN_PART;
else
{
status=HEAD_PART;
ano_frame_reset(&rec_frame_struct);
}
break;
}
case DATA_LEN_PART:
{
if(data>40) //<数据长度不可能大于40
{
status=HEAD_PART;
ano_frame_reset(&rec_frame_struct);
}
else
{
rec_frame_struct.data_len=data;
status=DATA_PART;
}
break;
}
case DATA_PART:
{
*(rec_frame_struct.data+cnt)=data;
cnt++;
if(cnt>=rec_frame_struct.data_len)
{
status=SC_PART; //<超过40,不再接收,强行结束,迁移到下一个状态
cnt=0;
}
break;
}
case SC_PART:
{
rec_frame_struct.sum_check=data;
status=AC_PART;
break;
}
case AC_PART:
{
rec_frame_struct.add_check=data;
//<完整的一帧接收完了,马上反馈信息给上位机
ano_parameter_feedback(&rec_frame_struct);
status=HEAD_PART; //<等待下一帧到来
break;
}
}
}
④接收一帧完成后的动作
static void ano_parameter_feedback(ano_frameStruct* rec_frame)
{
/*这样做是为了命名区分开发送和接收,看下列操作其实两者的参数信息同步*/
send_frame_struct.parameter->par_id=0;
rec_frame->parameter->par_id=0;
send_frame_struct.parameter->par_val=0;
rec_frame->parameter->par_val=0;
if(ano_check(rec_frame)) //<对于接收的完整通信帧,如果校验成功,表示帧的接收没有出现错误
{
rec_frame->parameter->par_id=rec_frame->data[0]+(rec_frame->data[1]<<8); //<记录上位机给定的参数ID
send_frame_struct.parameter->par_id=rec_frame->parameter->par_id;
if (rec_frame->function_id==0XE1)//<说明是上位机参数读取,下位机参数读取返回
{
switch (rec_frame->parameter->par_id)
{
case HWTYPE:
{
ano_send_parameter_frame(&send_frame_struct,PRO_ADDR);//将设备伪装成拓空者飞控
break;
}
case ID_INFO5:
{
ano_send_parameter_frame(&send_frame_struct,INFO5);
break;
}
case ID_INFO6:
{
ano_send_parameter_frame(&send_frame_struct,INFO6);
break;
}
case ID_INFO7:
{
ano_send_parameter_frame(&send_frame_struct,INFO7);
break;
}
case ID_INFO8:
{
ano_send_parameter_frame(&send_frame_struct,INFO8);
break;
}
case ID_INFO9:
{
ano_send_parameter_frame(&send_frame_struct,INFO9);
break;
}
case ID_INFO10:
{
ano_send_parameter_frame(&send_frame_struct,INFO10);
break;
}
///....其他参数id的情况,就交给读者自己定义了
}
return;
}
else if(rec_frame->function_id==0XE2) //<说明是上位机改变了下位机的值并校验返回
{
rec_frame->parameter->par_val=rec_frame->data[2]+(rec_frame->data[3]<<8)+(rec_frame->data[4]<<16)+(rec_frame->data[5]<<24);
send_frame_struct.parameter->par_val=rec_frame->parameter->par_val;
switch (rec_frame->parameter->par_id)
{
case ID_INFO5:
{
ID_INFO5=rec_frame->parameter->par_val;
break;
}
case ID_INFO6:
{
ID_INFO6=rec_frame->parameter->par_val;
break;
}
case ID_INFO7:
{
ID_INFO7=rec_frame->parameter->par_val;
break;
}
case ID_INFO8:
{
ID_INFO8=rec_frame->parameter->par_val;
break;
}
case ID_INFO9:
{
ID_INFO9=rec_frame->parameter->par_val;
break;
}
case ID_INFO10:
{
ID_INFO10=rec_frame->parameter->par_val;
break;
}
///....其他参数id的情况,就交给读者自己定义了
}
ano_send_check_frame(rec_frame->function_id,rec_frame->sum_check,rec_frame->add_check);
return;
}
}
}
变量INF05-INF010是用户自己定义的,取决于读者在项目中用于哪个参数的调试,这里只是给个模板。
六、展开
1.上位机的使用
官方使用教程
2.串口中断接收的优先级
发送逻辑不受影响。由于示例采用的是中断接收模式:
很遗憾,代码的交互任务受到中断优先级影响十分严重。因为在一个项目中,通信类任务往往是中频任务,这就意味着它经常被高频任务中断打断,在这样的情况下,正常得实现交互几乎不可能(主要在于MCU难以在频繁被打断的情况下接收完整的一帧数据),起码按照笔者上述的代码设计是难以实现在中频任务中(高频,中频和低频任务同时存在)仍旧能保持正常通信的。
所以,尽量确保串口接收中断的优先级要高,不被打断:
或者是,使用占用MCU资源更少的方法,来解决这个问题,也就是配合DMA,这样就不怕这个问题。但即使使用了DMA,在高频任务(比如电控的FOC任务)抢占的时候,作为中频或低频的交互任务,能够顺利运行,仍然对软件工程师来说是个巨大的挑战。
3.效率问题
无法否认一个事实,函数的高度封装,对于MCU这种主频低的芯片来说,所带来的影响是巨大的。好比如上述的frame_turn_to_array() 函数以及ano_send_flexible_frame(uint8_t id,int32_t x_coordinate,int32_t y_coordinate)函数,它们的设计思想就是把某一个操作封装好,然后通用化的,放置在一个功能函数里面,函数的调用多了,意味着进栈出栈频率高,故它的效率,笔者是没法保证的。
所以,上位机的函数,绝对绝对不能放进要求有固定控制周期的控制中断函数中,比如,一个PID控制周期是5ms,每5ms中断就产生,但是匿名上位机的发送函数和接收逻辑一来,MCU的处理反应远远大于5ms,这样你的控制项目,它的控制周期是不稳定的,严重影响你的带宽,也不可能做出一个稳定的系统。文章来源:https://www.toymoban.com/news/detail-413314.html
4.DMA接收模式
就交给大家能不能利用这个模式去开发啦!
笔者若后期有这个需求会更新到这里来。文章来源地址https://www.toymoban.com/news/detail-413314.html
到了这里,关于匿名上位机V7.12协议编程(基于STM32F407+CubeMX+UART外设通信)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!