赞
踩
官方的凌霄飞控程序里面将匿名通信协议封装成通用型了,初学者想要根据飞控的通信协议框架编写自己的数据传输比较困难,即通过使用匿名上位机的灵活格式帧,功能码(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部分代码即可实现发送自己的灵活格式帧数据。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。