1、今日主题
背景:
曾经在大三参加2017全国大学生电子设计竞赛,当时做的是四旋翼自主飞行器探测跟踪的题目,我是负责主控这一块的,中后期整个过程主要都是调试,在调试的过程中,我们需要将飞行器的各类数据上传至地面站上位机来观测飞行器的姿态,通过波形判断控制算法的鲁棒性,以便更好的调节控制参数。地面站有固定的的数据传输协议,根据协议进行编程,通过串口蓝牙无线传输,在电脑上就可以看到波形了。
问题:
由于各数据的数据类型不太一致,个数有一样的也有不一样的,针对每一次的调试就需要定义不同的函数,这可怎么办呢?用C语言编程思维惯性,第一反应当然就是根据需求定义多个不同的函数,粘贴复制,增加参数,粘贴复制,修改…,搞定!写完之后发现函数名要取得不一致,或者有的干脆用同一个函数名加数字123进行区别,调用的时候用哪个函数、要确定好每一个参数,一一对应,搞着搞着人都搞蒙了…
思考:
那要怎么解决这个问题呢?有没有什么办法能够进行优化的呢?
实践演练
解决思路:
1、学了C++自然就想到了C++支持函数重载,通过重载,对一个函数名赋予新的含义,让一个函数名可以多用。
1.1、于是将数据传输的函数名都定义为一样的:DataSendToUSART1();解决了定义多个函数名混淆不清的情况;
1.2、然后根据不同的需求定义不同的函数,实现不同的数据传输;例如:
// 传输三维姿态角
void DataSendToUSART1(float roll,float pitch,float yaw)
{
unsigned char i = 0;
unsigned char cnt = 0, sum = 0;
unsigned int mTemp;
unsigned char dataToSend[50];
dataToSend[cnt++]=0xAA;
dataToSend[cnt++]=0xAA;
dataToSend[cnt++]=0x01;
dataToSend[cnt++]=0;
mTemp = (int)(roll*100);
dataToSend[cnt++] = BYTE1(mTemp);
dataToSend[cnt++] = BYTE0(mTemp);
mTemp = (int)(pitch*100);
dataToSend[cnt++] = BYTE1(mTemp);
dataToSend[cnt++] = BYTE0(mTemp);
mTemp = (int)(yaw*100);
dataToSend[cnt++] = BYTE1(mTemp);
dataToSend[cnt++] = BYTE0(mTemp);
dataToSend[3] = cnt-4;
/* 和校验 */
for(i = 0; i < cnt; i++)
sum+= dataToSend[i];
dataToSend[cnt++] = sum;
/* 串口发送数据 */
for(i = 0; i < cnt; i++)
printf("%c", dataToSend[i]);
}
// 传输X ,Y,方向速度PID输出,高度
void DataSendToUSART1(int speed_X_PID,int speed_Y_PID,uint16_t height)
{
unsigned char i = 0;
unsigned char cnt = 0, sum = 0;
unsigned int mTemp;
unsigned char dataToSend[50];
dataToSend[cnt++]=0xAA;
dataToSend[cnt++]=0xAA;
dataToSend[cnt++]=0xF1;
dataToSend[cnt++]=0;
mTemp = (int)(speed_X_PID);
dataToSend[cnt++] = BYTE1(mTemp);
dataToSend[cnt++] = BYTE0(mTemp);
mTemp = (int)(speed_Y_PID);
dataToSend[cnt++] = BYTE1(mTemp);
dataToSend[cnt++] = BYTE0(mTemp);
mTemp = (uint16_t)(height);
dataToSend[cnt++] = BYTE1(mTemp);
dataToSend[cnt++] = BYTE0(mTemp);
dataToSend[3] = cnt-4;
/* 和校验 */
for(i = 0; i < cnt; i++)
sum+= dataToSend[i];
dataToSend[cnt++] = sum;
/* 串口发送数据 */
for(i = 0; i < cnt; i++)
printf("%c", dataToSend[i]);
}
在调用函数时,系统会根据给出的信息找到与之匹配的函数,然后进行调用。写完之后发现真的太棒了,这样我就不用一一去找什么时候用哪个函数了,只要直接调用就可以了
2、名字的问题是解决了,但是我的函数还是那么多个呀,程序还是很多重复的,不够精简,看起来还是很头疼啊,怎么办呢?
2.1、这时候我就根据传输的数据进行分类,我发现,有一些函数只是形参的类型不同,而函数的参数个数以及函数体(传输数据的程序)是一致的,于是想能不能定义一个通用的函数,这样就可以将只是形参类型不同的函数统一,大大的精简程序代码。
2.2、这时候,就到了C++的函数模板出场的时候了,通过定义一个通用的函数,函数的类型和形参类型不具体指定,用一个虚拟的类型来代表。例如:
template <typename T>
void DataSendToUSART1(T a,T b,T c)
{
unsigned char i = 0;
unsigned char cnt = 0, sum = 0;
unsigned int mTemp;
unsigned char dataToSend[50];
dataToSend[cnt++]=0xAA;
dataToSend[cnt++]=0xAA;
dataToSend[cnt++]=0x01;
dataToSend[cnt++]=0;
mTemp = (int)(a*100);
dataToSend[cnt++] = BYTE1(mTemp);
dataToSend[cnt++] = BYTE0(mTemp);
mTemp = (int)(b*100);
dataToSend[cnt++] = BYTE1(mTemp);
dataToSend[cnt++] = BYTE0(mTemp);
mTemp = (int)(c*100);
dataToSend[cnt++] = BYTE1(mTemp);
dataToSend[cnt++] = BYTE0(mTemp);
dataToSend[3] = cnt-4;
/* 和校验 */
for(i = 0; i < cnt; i++)
sum+= dataToSend[i];
dataToSend[cnt++] = sum;
/* 串口发送数据 */
for(i = 0; i < cnt; i++)
printf("%c", dataToSend[i]);
}
int main()
{
float roll = 1.0f,float pitch = 2.0f,float yaw = 3.0f;
DataSendToUSART1(roll,pitch,yaw);// 调用模板函数,此时T被float 取代
return 0;
}
只需在模板中定义一次,我们就可以用模板来代替所有函数体相同的函数。调用函数时,系统根据实参的类型来取代模板中的虚拟类型,从而实现不同函数的功能。
2.3、上述函数的类型参数都是一致的,但如果我要同时传输不同类型的数据呢,函数模板同样可以搞定,类型参数可以不止一个,可以根据功能需求确定个数:如:
template <typename T1, typename T2, typename T3>
至此,发现这功能简直不要太好用了吧,直接将发送数据的函数分为几类,对应哪一类就用那个函数模板就可以了。
小结
1、函数重载
针对实现同一类功能的函数,只是细节上有所不同,可以通过函数重载用同一函数名定义多个函数,这些函数的参数个数和类型、函数体都可以不同。
注意:参数个数、参数类型、参数顺序必须至少有一种不同。
2、函数模板
- 函数模板能够根据实参对参数类型进行推导
- 函数模板支持显示的指定参数类型
- 函数模板是C++中重要的代码复用方式
- 函数模板通过具体类型产生不同的函数
- 函数模板可以定义任意多个不同的类型参数
- 函数模板中的返回值类型必须显示指定
- 函数模板可以像普通函数一样重载
相关小知识
四轴飞行器调试的过程中有一个很重复的问题就是需要进行反复的参数调试,以求达到最好的控制参数,在这期间我们不可能调试一遍就修改一次程序进行烧录再调试,这样太浪费时间,同时我们需要观测相关数据曲线以判断控制过程的好坏,以此调节作为调节参数的依据。
我们当时的做法就是通过串口蓝牙将数据传送到匿名地面站上进行观测,同时通过自制遥控器进行参数调节,使用的是2.4G通讯,这样的好处是方便在电脑上观测飞行数据曲线,同时能够便捷的调试参数。