前言
本篇以STM32F407VET6主控为基础进行论述。 文章的目的在于记录和引导,传递一些编写通信类功能会用到的基本思想,以及组合这些功能的思想。 匿名上位机V7版本的功能在本篇没有全部写出来,而是针对本上位机常用的功能举例来描述。 相信读者通过仔细阅读笔者的思想引述和具体代码实现能够触类旁通。 限于笔者水平有限,文章中的一些错误还望批评指正。
一、匿名上位机的通信帧格式
其中DATA 数据内容中的数据,采用小端模式,低字节在前,高字节在后。 从图中可以一目了然,一个通信帧共有7个部分,每个部分都有规定。这直接影响了后续结构体的定义,及其成员的使用。 每个部分的内存基本单元为字节。
二、简单面向对象封装
1.封装
按照(一)中的通信帧结构定义结构体:
typedef struct
{
uint16_t par_id;
int32_t par_val;
}par_struct;
typedef struct
{
uint8_t head;
uint8_t target_addr;
uint8_t function_id;
uint8_t data_len;
uint8_t data[40];
uint8_t sum_check;
uint8_t add_check;
par_struct* parameter;
}ano_frameStruct;
通过结构体的“打包”,我们将一个通信帧当作一个对象来进行操作。 为什么data定义大小是40? 协议中有不同类型的帧,每个类型的帧里面,数据所需的长度都是不一样的,有一些类型的帧数据长度固定,有一些类型的帧可以自主选择帧数据的长度。 其中灵活数据帧是协议中最自由的协议,它的数据长度最大允许为40字节,仔细看文档可知,这同时也是所有类型帧中支持数据长度最长的,这直接决定了我们data部分数组定义的大小:
2.接口化
应用于匿名上位机的接口化非常简单,就是在编写过程函数的时候,函数的形参,都以通信帧结构体指针作为输入,这样就能实现对通信帧对象的统一操作。 比如:
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
#define SHUCHUAN_ADDR 0X10
#define GUANGLIU_ADDR 0X22
#define UWB_ADDR 0X30
#define IMU_ADDR 0X60
#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;
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;
send_frame_struct.target_addr=rec_frame_struct.target_addr=HOST_ADDR;
send_frame_struct.function_id=0XFF;
memset(send_frame_struct.data,0,40);
memset(rec_frame_struct.data,0,40);
}
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;
}
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.数据校验的逻辑
static void ano_check_calculate(ano_frameStruct* frame)
{
frame->sum_check=0;
frame->add_check=0;
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;
}
}
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外设接口,根据自己使用的串口修改就好。
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位的参数。 但是数据部分要求我们一个字节一个字节从低到高发送,所以我们需要对待传输的数据进行由低位到高位的字节截断。
#define BYTE0(temp) (*(char*)(&temp))
#define BYTE1(temp) (*((char*)(&temp)+1))
#define BYTE2(temp) (*((char*)(&temp)+2))
#define BYTE3(temp) (*((char*)(&temp)+3))
如果一个通信帧的内容全部定下来了,我们需要把通信帧转为从低位到高位的线性数组:
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.向上位机发送字符串
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;
send_frame_struct.data[cnt++]=color;
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);
}
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;
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.向上位机发送灵活数据帧
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;
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);
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);
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)
{
static uint8_t data=0;
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);
}
}
5.接收状态分析
五个状态:正在接收帧头,正在接收帧目标地址,正在接收帧功能码ID,正在接收帧数据部分长度信息,正在接收帧数据部分,正在接收帧的和校验,正在接收帧的附加校验 简单描述一下接收过程: ① 初始状态(它的选定很重要): 等待接收帧头,如果接收到的不是0XAA,认为不是上位机发过来的数据,停留于该阶段,如 绿色自返箭头所示。 ②接收目标地址:接收的地址必定是硬件地址那8种,可以选择性根据这一特征进行判断,来决定是否迁移;笔者这里没有做判断,接收完地址,直接迁移到下一个状态。 ③接收功能码ID:必定是0XE0,0XE1或0XE2其一,若不是,通信帧发生错误,迁移到初始状态;若是,迁移到下一步。 ④接收数据部分长度信息:必定是小于或等于40,如果小于0或大于40,通信出错,迁移到初始状态;若是,迁移到下一步。 ⑤接收数据:在到达数据长度前,都 自返于此状态接收数据,接收完之后呢,就迁移到下一个状态; ⑥接收和校验 ⑦ 接收附加校验: 到这时候,完整的一帧,已经接收完了 ,执行反馈操作,初始化通信帧,等待下一个接收到来。
五、状态机思想应用于接收逻辑
1.头文件增加类型定义
enum FRAME_PART
{
HEAD_PART=0,
ADDR_PART,
ID_PART,
DATA_LEN_PART,
DATA_PART,
SC_PART,
AC_PART
};
2.参数读写类帧的通信
①反馈给上位机的校验帧
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;
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);
}
②反馈给上位机的参数读取返回帧
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);
}
③状态机
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)
{
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;
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)
{
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;
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);
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;
}
}
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;
}
}
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,这样你的控制项目,它的控制周期是不稳定的,严重影响你的带宽,也不可能做出一个稳定的系统。
4.DMA接收模式
就交给大家能不能利用这个模式去开发啦! 笔者若后期有这个需求会更新到这里来。
|