一、介绍
官方的凌霄飞控程序里面将匿名通信协议封装成通用型了,初学者想要根据飞控的通信协议框架编写自己的数据传输比较困难,即通过使用匿名上位机的灵活格式帧,功能码(0XFx)进行数据传输。
接下来,我将给大家介绍如何添加代码,完成自己的数据上传到上位机的功能。
二、具体操作
首先,我们需要对凌霄飞控程序有个基本了解,这里我不多介绍,只介绍关于通信协议的部分。
我们查找源码,可以发现飞控数据与上位机通信是在1ms定时器里面通过使用ANO_LX_Data_Exchange_Task()函数进行的
我们进一步查找可以发现它里面都是调用Check_To_Send()函数,有很多这个函数,函数的参数是匿名通信的功能码,所以这里使用不同的功能码就可以传输功能码对应的“数据类型”,光流数据、GPS数据、CMD指令等等。
我们找到了第一个需要增加语句的部分,我们要在这个函数里面添加一个功能码为0xF1的语句
Check_To_Send(0xF1); //灵活格式帧(Open_MV数据)
进一步寻找Check_To_Send()函数的作用,里面有很多结构体参数判断,这里我们先不管这部分,后面会说明这部分的功能,直接找到最重要的函数Frame_Send()
注意Frame_Send()函数的参数frame_num,这个是我们在Check_To_Send()函数填写的功能码,注意这一点,非常重要,后面都是根据功能码来填写参数的。
我们进一步寻找Frame_Send()函数的具体功能,查看之后我们应该知道这个函数是匿名通信协议框架,具体情况观看B站UP主“匿名_茶不思”大佬的匿名通信协议介绍
匿名上位机V7版--0基础教程3--从0开始写匿名协议发送源码_哔哩哔哩_bilibili
static void Frame_Send(u8 frame_num, _dt_frame_st *dt_frame)
{
u8 _cnt = 0; //地址偏移: 每写完一次自动++ 实现地址偏移
send_buffer[_cnt++] = 0xAA; //帧头 [HEAD] (固定值)
send_buffer[_cnt++] = dt_frame->D_Addr; //目标地址 [D_ADDR](将发送框架里面的地址存入缓存数组)
send_buffer[_cnt++] = frame_num; //功能码 [ID] (0X01、0X30等等)
send_buffer[_cnt++] = 0; //数据长度 [LEN]
//==填充需要发送的数据
Add_Send_Data(frame_num,&_cnt, send_buffer);//发送数据 [DATA] (根据功能码对应填充格式)
//数据长度更新
send_buffer[3] = _cnt - 4;
//==进行数据校验 和校验 + 附加校验
u8 check_sum1 = 0, check_sum2 = 0;
for (u8 i = 0; i < _cnt; i++)
{
check_sum1 += send_buffer[i]; //和校验
check_sum2 += check_sum1; //附加校验
}
send_buffer[_cnt++] = check_sum1; //将校验结果存入数组
send_buffer[_cnt++] = check_sum2;
//==校验结果数据传递
if (dt.wait_ck != 0 && frame_num == 0xe0)
{
dt.ck_back.ID = frame_num; //ID
dt.ck_back.SC = check_sum1; //和检验
dt.ck_back.AC = check_sum2; //附加校验
}
//==发送数组函数(需要发送的数组,数组长度)
ANO_DT_LX_Send_Data(send_buffer, _cnt);
}
在这个函数里面我们最需要注意的就中间的Add_Send_Data()函数,这个函数是填写具体数据的,我们需要根据自己的需要填写数据
我们进入Add_Send_Data函数
static void Add_Send_Data(u8 frame_num, u8 *_cnt, u8 send_buffer[])
{
s16 temp_data;
s32 temp_data_32;
//根据需要发送的帧ID,也就是frame_num,来填充数据,填充到send_buffer数组内
switch (frame_num)
{
case 0x00: //CHECK返回 数据校验帧
{
send_buffer[(*_cnt)++] = dt.ck_send.ID;
send_buffer[(*_cnt)++] = dt.ck_send.SC;
send_buffer[(*_cnt)++] = dt.ck_send.AC;
}
break;
case 0x0d: //电池数据
{
for (u8 i = 0; i < 4; i++)
{
send_buffer[(*_cnt)++] = fc_bat.byte_data[i];
}
}
break;
case 0x30: //GPS数据
{
//
for (u8 i = 0; i < 23; i++)
{
send_buffer[(*_cnt)++] = ext_sens.fc_gps.byte[i];
}
}
break;
case 0x33: //通用速度测量数据
{
//
for (u8 i = 0; i < 6; i++)
{
send_buffer[(*_cnt)++] = ext_sens.gen_vel.byte[i];
}
}
break;
case 0x34: //通用距离测量数据
{
//
for (u8 i = 0; i < 7; i++)
{
send_buffer[(*_cnt)++] = ext_sens.gen_dis.byte[i];
}
}
break;
case 0x40: //遥控数据帧
{
for (u8 i = 0; i < 20; i++)
{
send_buffer[(*_cnt)++] = rc_in.rc_ch.byte_data[i];
}
}
break;
case 0x41: //实时控制数据帧
{
for (u8 i = 0; i < 14; i++)
{
send_buffer[(*_cnt)++] = rt_tar.byte_data[i];
}
}
break;
case 0xe0: //CMD命令帧
{
send_buffer[(*_cnt)++] = dt.cmd_send.CID;
for (u8 i = 0; i < 10; i++)
{
send_buffer[(*_cnt)++] = dt.cmd_send.CMD[i];
}
}
break;
case 0xe2: //PARA返回
{
temp_data = dt.par_data.par_id;
send_buffer[(*_cnt)++] = BYTE0(temp_data);
send_buffer[(*_cnt)++] = BYTE1(temp_data);
temp_data_32 = dt.par_data.par_val;
send_buffer[(*_cnt)++] = BYTE0(temp_data_32);
send_buffer[(*_cnt)++] = BYTE1(temp_data_32);
send_buffer[(*_cnt)++] = BYTE2(temp_data_32);
send_buffer[(*_cnt)++] = BYTE3(temp_data_32);
}
break;
case 0xF1: //灵活格式数据
{
//==将int型数据拆成高8位和低8位进行发送
send_buffer[(*_cnt)++] = BYTE0(openmv.red_receive_distance );
send_buffer[(*_cnt)++] = BYTE1(openmv.red_receive_distance);
}
break;
default: break;
}
}
我们发现里面使用Switch-case语句进行选择,选择条件就是frame_num——功能码。我们再看case里面的语句,发现有的使用for语句,有的直接使用 BYTE0(temp_data_32),看到BYTE1()是不是比较熟悉了?没错,这个就是例程里面的数据拆分函数,将数据拆分完后存到数组里面
虽然前面有使用for循环进行数据填充的,但是我们的灵活格式帧因为数据长度不确定,所以直接用最简单的方式即可——像例程一般,直接根据自己数据的类型进行数据拆分
case 0xF1: //灵活格式数据
{
//==将int型数据拆成高8位和低8位进行发送
send_buffer[(*_cnt)++] = BYTE0(openmv.red_receive_distance );
send_buffer[(*_cnt)++] = BYTE1(openmv.red_receive_distance);
}
break;
我这里是传输一个s16型的数据,所以需要拆分两次。这部分代码根据自己的实际需要进行修改,我相信看过视屏的同学应该能自己编写这一部分代码了。
这部分语句就是需要我们自己添加的第二部分代码。
注意因为这个函数里面没有数据这个参数选项,我们需要使用结构体进行数据传递:openmv.red_receive_distance,使用结构体进行参数传递是最方便的。还需要注意的一点是,我们通信函数是放在1ms定时器里面的,结构体数据的更新也尽量要同步,所以我们可以将结构体参数更新放到1ms调度器里面,这样数据就不会不同步了
接下来回到Frame_Send()函数,在例程中我们需要自己计算数据的长度,并事先赋值好,这样比较容易出错,所以在凌霄飞控的通信协议框架中,我们是再填写完数据之后再确定数据长度的,在缓存数组的第四位send_buffer[3]存放的是数据长度,我们之前是直接给0,填写完数据之后我们就可以更新数据长度了
//数据长度更新
send_buffer[3] = _cnt - 4;
下面的和校验和附加校验,然后是将校验结果存放到相关结构体里面,这部分应该没问题。
根据例程,我们应该使用数组发送函数进行数据发送,通信就完成了
//==发送数组函数(需要发送的数组,数组长度)
ANO_DT_LX_Send_Data(send_buffer, _cnt);
这里使用的是一个嵌套重映射:ANO_DT_LX_Send_Data()函数 -> UartSendLXIMU()函数 -> DrvUart5SendBuf()函数,这3个函数是等效的,为什么要这么麻烦的重重嵌套,我也不知道,也许是方便修改成自己的函数。
到这里其实一个通信函数已经写好了,在例程中,我们是直接将框架函数放到调度器里面的,但是因为凌霄程序里面使用了层层调用,我们不是直接放在调度器里面的,而是放在1ms定时器里面,所以我们需要添加一部分代码。
我们回到Check_To_Send()函数,这里面一堆结构体参数
static void Check_To_Send(u8 frame_num)
{
//==定时触发发送
if (dt.fun[frame_num].fre_ms)
{
//==初始相位小于定时发送周期,等待时间
if (dt.fun[frame_num].time_cnt_ms < dt.fun[frame_num].fre_ms)
{
dt.fun[frame_num].time_cnt_ms++;
}
else
{
dt.fun[frame_num].time_cnt_ms = 1;
dt.fun[frame_num].WTS = 1; //标记等待发送
}
}
else
{
//等待外部触发
}
//==等待发送
if (dt.fun[frame_num].WTS)
{
dt.fun[frame_num].WTS = 0;
//==进行数据填充、实际发送
Frame_Send(frame_num, &dt.fun[frame_num]);
}
}
我们对结构体参数进行分析,发现它判断的值其实是在ANO_DT_Init()函数里面设置的,
void ANO_DT_Init(void)
{
//========定时触发
//电压电流数据
dt.fun[0x0d].D_Addr = 0xff;
dt.fun[0x0d].fre_ms = 100; //触发发送的周期100ms
dt.fun[0x0d].time_cnt_ms = 1; //设置初始相位,单位1ms
//遥控器数据
dt.fun[0x40].D_Addr = 0xff;
dt.fun[0x40].fre_ms = 20; //触发发送的周期100ms
dt.fun[0x40].time_cnt_ms = 0; //设置初始相位,单位1ms
//灵活格式帧
dt.fun[0xF1].D_Addr = 0xff;
dt.fun[0xF1].fre_ms = 500; //触发发送的周期500*1ms
dt.fun[0xF1].time_cnt_ms = 0; //设置初始相位,单位1ms
//========外部触发
//GPS 传感器信息
dt.fun[0x30].D_Addr = 0xff;
dt.fun[0x30].fre_ms = 0; //0 由外部触发
dt.fun[0x30].time_cnt_ms = 0; //设置初始相位,单位1ms
//通用速度型传感器数据
dt.fun[0x33].D_Addr = 0xff;
dt.fun[0x33].fre_ms = 0; //0 由外部触发
dt.fun[0x33].time_cnt_ms = 0; //设置初始相位,单位1ms
//通用测距传感器数据
dt.fun[0x34].D_Addr = 0xff;
dt.fun[0x34].fre_ms = 0; //0 由外部触发
dt.fun[0x34].time_cnt_ms = 0; //设置初始相位,单位1ms
//实时控制帧
dt.fun[0x41].D_Addr = 0xff;
dt.fun[0x41].fre_ms = 0; //0 由外部触发
dt.fun[0x41].time_cnt_ms = 0; //设置初始相位,单位1ms
//CMD 命令帧
dt.fun[0xe0].D_Addr = 0xff;
dt.fun[0xe0].fre_ms = 0; //0 由外部触发
dt.fun[0xe0].time_cnt_ms = 0; //设置初始相位,单位1ms
//参数写入、参数读取返回
dt.fun[0xe2].D_Addr = 0xff;
dt.fun[0xe2].fre_ms = 0; //0 由外部触发
dt.fun[0xe2].time_cnt_ms = 0; //设置初始相位,单位1ms
}
这里设置了定时触发和外部触发,像电池电压、遥控数据,它们是变化的,我们是需要它经常返回上位机的。像光流、GPS、CMD命令等这些数据是外部传感器发送给飞控的,不受飞控控制,飞控只是数据传输到上位机的中介,所以设置为外部触发。
我们自己发送的灵活格式帧,是需要它定时返回上位机的(跟放在调度器里面一样),所以我们需要配置为定时触发,根据需要设置dt.fun[0xF1].fre_ms的值。
//灵活格式帧
dt.fun[0xF1].D_Addr = 0xff;
dt.fun[0xF1].fre_ms = 500; //触发发送的周期500*1ms
dt.fun[0xF1].time_cnt_ms = 0; //设置初始相位,单位1ms
这里是需要添加的第三部分代码。
在Check_To_Send()函数里面使用if语句判断dt.fun[0xF1].fre_ms,如果大于0,就进入定时触发,标记dt.fun[frame_num].WTS标志位,否则等待外部触发。
if (dt.fun[frame_num].fre_ms)
{
//==初始相位小于定时发送周期,等待时间
if (dt.fun[frame_num].time_cnt_ms < dt.fun[frame_num].fre_ms)
{
dt.fun[frame_num].time_cnt_ms++;
}
else
{
dt.fun[frame_num].time_cnt_ms = 1;
dt.fun[frame_num].WTS = 1; //标记等待发送
}
}
定时时间到了,dt.fun[frame_num].WTS标志位为1,下面即进入发送框架函数
if (dt.fun[frame_num].WTS)
{
dt.fun[frame_num].WTS = 0;
//==进行数据填充、实际发送
Frame_Send(frame_num, &dt.fun[frame_num]);
}
这部分功能实际跟直接将发送框架函数放到调度器里面是相同的,但是由于这个是一个通用型的发送框架,我们需要做适当的调整。
凌霄飞控的通用通信协议函数介绍到这里。我们只需要添加上面3部分代码即可实现发送自己的灵活格式帧数据。