1. 引言
经过这一年半中对实验室中流传代码的质量的考察,我觉得有必要写一篇文章来矫正一些恶劣习惯,以及向萌新们传授一些知识和经验,本文主要针对嵌入式小应用的编程规范。我自认不是专家,只是普通的本科非计算机专业学生,故写作本文实为抛砖引玉。可能部分描述有错误,或不够严谨,欢迎批评指正。
2. 代码规范
我现在负责今年飞思卡尔智能车比赛光电追逐组软件部分的整体框架,在完成目前4个版本的过程中,也为每一个增删改动写了日志,算是汲取去年对工程把控不足的教训。在ver0.10
完成后我做了如下的备注:
由于要复习考试,并无时间完善注释,将在以后完成这项工作。如有意愿协助,请一定按照以下规则进行:
- 无需文档注释(类似超核固件库的形式),因为暂时没有意义,如想要学习这种注释方法,可以参考我后续上传的两个工程;
//
形式的注释后空一格或在其上一行对其行首,/**/
的形式在*
后空一格,前空一格其余基本和//
相同,特殊情况经由讨论决定,即:
bar = Foo(); //description
//description
*shit = Fuck(&you);
/* description */
suck &= ~(cug << ASS_ASS_IN | CREED);
- 对齐!!!
添加代码时一定注意以下几点:
- 大括号请按照后空一格
{
,换行}
的形式书写,即:
int8_t Foo(int8_t, uint8_t) {
//Do something
}
- 请使用
c99
定义整数类型(即类似uint8_t
的形式,参见stdint.h
);- 尽可能使用按位的标志,并将每位的含义以宏的形式列表;
- 不容忍任何可以用局部变量或输入形参代替的全局变量的存在;
- 不容忍任何
Warning
;- 函数内应尽可能不出现任何常数常量,相应的宏慎重考虑是汇总到参数表还是列在本模块头部;
- 决不容忍任何形式的非tab整数倍缩进,以及行末和行首多余的空格!!!
请考虑清楚新增元素的定位,准确写到相应的模块里,宁可额外扩展也不要胡乱塞进原模块和封装,命名则须严格按照以下规则:
- 函数名用大写首字母的驼峰命名(没有下划线,下同);
- 变量名用小写首字母的驼峰命名;
- 宏用由下划线隔开的大写单词拼接;
- 布尔变量不能使用简单的类似
flag
的名称,一般可为isxxx
,hasxxx
等形式,特殊情况讨论斟酌;- 不要用汉语拼音!!!
以上。还有待补充。
也许萌新以及一些“实用主义者”会觉得事儿多,但这些要求还真不能算是有多苛刻。
C
体系对于代码的格式放得很开,这成就了它很强的灵活性,但同时也放纵了水平不那么高的程序员生产大量难以阅读的代码。
C
代码的书写有很多风格,花样百出,各有各的道理。但无论哪种风格,都一定会排斥和避免特定的那么一系列的行为。我用搜索引擎查了一下,第一页就有一篇很不错的文章,可以看看:C代码规范
另外,在花括号是否换行的问题上,一直以来都是争议不断的。有不换行的,有换行的,还有换行之后加tab
的,各种各样的写法都有——这里顺带提一句,Python
中取消花括号而以缩进的方式来表示代码块,而Go
(被很多编程语言专家骂得很惨的Google
搞出来的新语言)中则是把花括号换行认作语法错误,他们通过这种方式来限制程序员的书写,而C
是自由的——我个人倾向于使用写Java
时比较习惯的不换行的写法。采用哪种书写风格(这里提到的所有“风格”指的仅仅是书写风格,而不是“C
风格的C++
”这样的句子中的所说的“风格”)并不是很要紧,但风格的一致很重要。选择这种类Java
的写法,只是因为定一个规范总是好的罢了(毕竟我们实验室压根就没有所谓的规范)。而在修改工程量较大的旧代码时,我主张遵循这样的原则——配合其书写风格,而不是坚持自己的。
一定记住,这项工程的开发人员不仅仅是你一人,参考、学习并提出建议者更是大有人在,无论是考虑到团队合作、流传后世还是书写美感(关于更多详细的代码书写艺术请参考书籍或观察学习一些优美的代码),这都绝不是无意义的。试想一个只有提交者自己才能看懂的代码,该怎么维护?
我们写代码的意义是什么?除了算法,不就是为了实现更好的feature
、更好的interface
吗?看着头痛看不懂,用起来束手束脚,还容易出BUG,这怎么能叫好用。即使哪一天能正常运行了,我认为也是——在这种环境、这种条件下,恰好可以运行一会儿——而已。
实验室的dalao们普遍有一种非常错误的观念,即:能运行>>>>一切,一切都是为了这个装置能运行,最首要的任务是让它跑起来。某种程度上这当然没错,但是这个理念在执行的时候就变成了:跑起来就万事大吉了,别的我就不管了,代码就这样吧,也不改了。于是留下一堆垃圾。
下面给个例子:
#include "gpio.h"
#include "common.h"
#include "variable.h"
#include "picture_get.h"
#include "picture_handle.h"
#include "speed_direction_pid.h"
#include "infrared_start.h"
/*图像数字特征变量______________________________*/
extern int odd_even_flag;//奇偶场变量(方便发图)
extern int h,k; //h为实际行数,k为采集数组行数
extern int ph;//动态前瞻行数
extern int ph1;//实际PH
extern int left_max_error;//最大左偏差
extern int light_max_error;//最大右偏差
extern unsigned char table[row][line]; //采集原始图像二维数组
extern int left[row]; //左边界数组
extern int light[row]; //右边界数组
extern int bottom[row]; //中线数组
extern int bottomflag[row]; //下一行寻边界的起始点(追踪)
//数学特征1(斜率)
extern double left_slope[row]; //左边斜率数组
extern double light_slope[row]; //右边斜率数组
extern double left_zero[row]; //左边零点数组
extern double light_zero[row]; //右边零点数组
extern int slope_a;
extern int slope_b;
extern int slope_x;
//数学特征2(拐点)
extern unsigned char left_inflexion[row]; //左边拐点数组
extern unsigned char light_inflexion[row]; //右边拐点数组
extern signed char left_trend[row]; //左边趋势数组
extern signed char light_trend[row]; //右边趋势数组
extern signed char left_lasttrend; //左边之前趋势
extern signed char light_lasttrend; //右边之前趋势
/*赛道类型变量__________________________*/
extern int zx_flag;//直线
extern unsigned int zx_r;//相关系数
extern double zx_r_a;//相关系数
extern double zx_r_b;//相关系数
extern int sw_flag;//s弯
extern int dw_flag;//大弯
extern int zj_flag;//直角
extern int tc_flag;//停车
extern int dx_flag;//单线
extern int hk_flag;//黑块
extern int sz_flag;//十字
extern int za_flag;//障碍
extern int pd_flag;//坡道
//停车允许变量
extern int tc_zflag;
extern int zq_flag;//自启动变量
extern int l_hw;//两个红外
//单线判断部分变量
extern int dx_flag1;//单线左边标志及其坐标
extern int dx_flag2;//单线右边标志及其坐标
extern int dx_flag3;//(单线靠边标志)
extern int dx_flag4;//(单线补线标志)
extern int dx_flageh;//(单线补线起始行)
extern int dx_zflag;//本场不允许检测单线变量
extern int dx_zxflag;//直线情况下单线限制条件标志位
extern int dx_sflag;//本场单线起始行
extern int dx_eflag;//本场单线结束行
extern int last_dx_flag;//本场本行之前的单线标志
extern int dx_k[row];//本行单线宽度
extern int za_s;//障碍启示行
extern int za_e;//障碍结束行
extern int dx_first;
extern int dx_b_cnt;
//直角判断部分
extern int zj_black_sum;
extern int zj_black_sum_h;
extern int zj_flag1;
extern int zj_flag2;
extern int left_zj;
extern int light_zj;
extern int left_zj_h;
extern int light_zj_h;
//黑块补线变量部分
extern int black_starth;
extern int black_startflag;
extern int youblack_starth;
extern int youblack_startflag;
/*舵机PID部分参数_______________________*/
extern float d; //角度(偏差值)
extern int PWM; //舵机pwm
extern float error,lasterror,preverror,derror;
extern float kp,kd;//舵机控制PD
extern float error_sum;
extern int f_count;//电机平滑变量
extern int last_infrared;
extern int now_infrared;
extern int cnt;
int cntb=0;
extern int cnt1;
extern int cnt2;
//小S判断部分
int S_left=0;
int S_light=0;
int S_bottom=0;
int S_error=0;
/*小S最小二乘法拟合直线变量start*/
float K=0; //拟合直线的斜率
float R=0; //拟合直线的截距
float x_sum_average=0; //数组X的所有元素求和并求平均值
float y_sum_average=0; //数组Y的所有元素求和并求平均值
float x_square_sum=0; //数组X求每个元素的平方的平均值
float x_multiply_y=0; //数组X和Y对应元素的乘积求和
/*小S拟合变量end*/
int hong1=0;
int hong2=0;
/*图像采集部分(场行中断处理函数)______________*/
//****************************************************************************
// 函数名:GpioInterruptVSYN(void)
// 功能:摄像头场中断处理函数
// 说明:(1)偶场采集图像初步计算(2)奇场补线并辨别图像类型
//*****************************************************************************/
void GpioInterruptVSYN(uint32_t pinxArray)
{
cnt1++;
if(cnt1>=3)
{
cnt1=0;
}
if(cnt1>=2)
PCout(14)=1;
else
{
PCout(14)=0;
if((cnt1==1)&&(cnt2<=2))
{
if(cnt>110)
hong1=1;
else
hong1=0;
if(cntb>110)
hong2=1;
else
hong2=0;
if(l_hw==0)
{
if((cnt>110)&&(cntb>110))
{
cnt2++;
if(cnt2==3)
{
cnt2=0;
now_infrared=1;
}
//now_infrared=1;
}
else
{
cnt2=0;
now_infrared=0;
}
if((last_infrared==1)&&(now_infrared==0))
{
if(!((tc_flag==0)&&(tc_zflag==1)))
{
if(tc_flag)//发车
{
if(!zq_flag)
tc_flag=!tc_flag;
}
else
tc_flag=!tc_flag;
}
}
last_infrared=now_infrared;
}
else
{
if((cnt>110)||(cntb>110))
{
cnt2++;
if(cnt2==3)
{
cnt2=0;
now_infrared=1;
}
//now_infrared=1;
}
else
{
cnt2=0;
now_infrared=0;
}
if((last_infrared==1)&&(now_infrared==0))
{
if(!((tc_flag==0)&&(tc_zflag==1)))
{
if(tc_flag)//发车
{
if(!zq_flag)
tc_flag=!tc_flag;
}
else
tc_flag=!tc_flag;
}
}
last_infrared=now_infrared;
}
}
}
cnt=0;
cntb=0;
//odd_even_flag=GPIO_ReadBit(HW_GPIOB,20);
odd_even_flag=0;
// if(odd_even_flag) PAout(4)=0;
// else PAout(4)=1;
h=0;
k=0;
if(!odd_even_flag)
{
dx_flag=0;
dx_flag3=0;
dx_flag4=0;
dx_zflag=0;
dx_zxflag=0;
dx_sflag=0;
dx_eflag=0;
last_dx_flag=0;
dx_first=0;
dx_b_cnt=0;
left_zj_h=0;
light_zj_h=0;
black_startflag=0;
youblack_startflag=0;
dw_flag=0;
sz_flag=0;
za_flag=0;
za_s=0;
za_e=0;
//pd_flag=0;
zj_black_sum_h=0;
left_zj=0;
light_zj=0;
S_left=0;
S_light=0;
S_bottom=0;
S_error=0;
// /*小S最小二乘法拟合直线变量初始化start*/
// K=0; //拟合直线的斜率
// R=0; //拟合直线的截距
// x_sum_average=0; //数组X的所有元素求和并求平均值
// y_sum_average=0; //数组Y的所有元素求和并求平均值
// x_square_sum=0; //数组X求每个元素的平方和
// x_multiply_y=0; //数组X和Y对应元素的乘积求和
// /*小S拟合变量end*/
}
int y,z;
/*相关系数R判断直线start*/
zx_r=0;
if((bottom[34]!=bottom[0]))
{
zx_r_a=34.0/(bottom[34]-bottom[0]);
zx_r_b=34.0*bottom[0]/(bottom[0]-bottom[34]);
for(y=0;y<row;y++)
{
z=(bottom[y]-(y-zx_r_b)/zx_r_a);
if(z>0)
zx_r+=z;
else
zx_r-=z;
}
}
else
{
for(y=0;y<row-5;y++)
{
z=bottom[y]-bottom[0];
if(z>0)
zx_r+=z;
else
zx_r-=z;
}
}
if(zx_r>500)
zx_r=500;
//if((zx_r>=0)&&(zx_r<2000))
if((zx_r<90)&&(!last_infrared))
//if(zx_r<90)
zx_flag=1;
else
zx_flag=0;
/*相关系数R判断直线end*/
}
//****************************************************************************
// 函数名:GpioInterruptHREF(void)
// 功能:摄像头行中断处理函数
// 说明:(1)偶场采集图像初步计算(2)奇场补线并辨别图像类型
//*****************************************************************************/
void GpioInterruptHREF(uint32_t pinxArray)
{
int x,y;
double z;
if(!odd_even_flag)
{
if((k<=49)&&(h>19))//计算中心黑线
{
if(h%5==0)//采集图像数据
{
for(x=0;x<=195;x++);
for(x=line-1;x>=0;x--)
table[k][x]=PTD->PDIR;
}
if(h%5==1)//寻找左右边界(&判断是否为单线,大弯,障碍)
{
Drop_Search1_L(k);
Drop_Search1_R(k);
last_dx_flag=dx_flag2;
}
if(h%5==2)
{
Slope_Figure(k);//计算左右边界斜率
Inflexion_Figure(k);//判断左右边界是否为拐点
Picture_Scan3(k);//直角判断部分(&冲出跑道判断部分)
}
if(h%5==3)
{
Picture_Scan2(k);//十字赛道判别
}
if(h%5==4)
{
Picture_Scan(k);//单线补线部分
Picture_Scan4(k);//直线判断部分,计算本场搜寻单线条件,计算中线斜率
if(za_flag)
{
bottom[k]=bottom[za_e-1];
bottomflag[k]=bottom[k];
}
if(dw_flag==5)
{
bottom[k]=0;
}
if(dw_flag==6)
{
bottom[k]=line-1;
}
if(k<35)
{
if(k==0)
{
S_left = bottom[k];
S_light = bottom[k];
}
else
{
if(bottom[k]>S_light)
S_light = bottom[k];
if(bottom[k]<S_left)
S_left = bottom[k];
}
}
else
{
if((bottom[k]>S_left)&&(bottom[k]<S_light))
{
S_bottom++;
}
}
S_error +=(standard-bottom[k]);
// /*小S最小二乘法拟合直线start*/
// x_sum_average+=bottom[k];
// y_sum_average+=k;
// x_square_sum+=(bottom[k]*bottom[k]);
// x_multiply_y+=(bottom[k]*k);
// /*小S拟合变量end*/
if(k==49)//舵机控制
{
// /*相关系数R判断直线start*/
// zx_r=0;
// if((bottom[49]!=bottom[0]))
// {
// zx_r_a=49.0/(bottom[49]-bottom[0]);
// zx_r_b=49.0*bottom[0]/(bottom[0]-bottom[49]);
// for(y=0;y<row;y++)
// {
// z=(bottom[y]-(y-zx_r_b)/zx_r_a);
// if(z>0)
// zx_r+=z;
// else
// zx_r-=z;
// }
// }
// else
// {
// for(y=0;y<row;y++)
// {
// z=bottom[y]-bottom[0];
// if(z>0)
// zx_r+=z;
// else
// zx_r-=z;
// }
// }
// //if((zx_r>=0)&&(zx_r<2000))
if(zx_r<2000)
zx_flag=1;
else
zx_flag=0;
//
//
//
//
// /*相关系数R判断直线end*/
if(S_bottom>8)
sw_flag = 1;
else
sw_flag = 0;
//sw_flag = 1;
if((sw_flag)&&(!zj_flag)&&(S_light-S_left>5))
error= S_error/50.0;
else
error=(10*standard-bottom[ph+5]-bottom[ph+4]-bottom[ph+3]-bottom[ph+2]-bottom[ph+1]-bottom[ph]-bottom[ph-1]-bottom[ph-2]-bottom[ph-3]-bottom[ph-4]-bottom[ph-5])/10.0;
duoji_control();
// /*小S最小二乘法拟合直线start*/
// x_sum_average=x_sum_average/row;
// y_sum_average=y_sum_average/row;
// x_square_sum=x_square_sum;
// x_multiply_y=x_multiply_y;
// K = ( x_multiply_y - row * x_sum_average * y_sum_average)/( x_square_sum - row * x_sum_average*x_sum_average );
// R = y_sum_average - K * x_sum_average;
//
// for(y=0;y<row;y++)
// bottom[y]=(y-R)/K;
// /*小S拟合变量end*/
}
k++;
}
}
h++;
}
if(!GPIO_ReadBit(HW_GPIOB,11))
cnt++;
if(!GPIO_ReadBit(HW_GPIOB,10))
cntb++;
}
先不考虑它的“实用性”,单看这个代码本身,我完全可以说,能找出它一万个毛病。如果你觉得看不懂,更多的是因为这个代码写的实在太烂了而不是你的理解能力不够。还有一点很要命,这篇代码是以MDK4
默认的ANSI
编码写的而不是UTF-8
,加之作者极喜欢写中文注释,因此会产生一些的问题(这里的复制粘贴是转了码的)。
下面说说这个代码。
首先在头文件引用的部分,文件名使用的是小写+下划线命名方式,不提词汇选用的问题,还算能够接受。但紧接着的这一百来行,不知读者看了有没有种抓狂的感觉。
事实上这大量冗余的extern
关键词修饰在每个实现文件里都有一百行。它使用的结构是将所有全局变量(无论是否值得声明为全局)悉数定义在variable.c
中,然后在每个模块中进行extern
引用。
这里面存在着数不胜数的随意、杂乱、毫无章法的命名,带有中文拼音简写和不知是什么语言的前缀,甚至还有毫无任何意义的h
,cnt
,cnt1
等等。
后面,虽然这个文件没有体现出来,但是整个工程中的函数的命名与参数的形式也是各路妖魔鬼怪一应俱全。大量的无参数,无意义参数,无返回值体现了这个架构极低的可读性、可移植性和健壮性。
函数的具体实现也是有着大量的逻辑冗余,低效率的处理方式,乱七八糟的低独立性的小模块,以及几乎一句正常、有效的注释都没有。实在惨不忍睹。
最开始我打算将这些东西做一点兼容性处理,直接移植到我的工程里——很快我就放弃了,因为这玩意儿已经病入膏肓,不如我自己重写了。
虽然从很多细节上可看出作者对各种复杂逻辑处理考虑的周密,但我更希望dalao能够把这代码写的更像给人看的一点。我相信,写成这个模样,即使是他们自己调试起来也会极费劲的。
3. 构架技巧
下面谈谈我自己的代码,比较浅薄,别笑。
main
中的处理尽量少,仅包含Init
和Loop
的轮廓(与arduino
中setup
和loop
相仿):
#include "MainProc.h"
#include "DataComm.h"
#include "ImgProc.h"
int main() {
MainInit();
while(1) {
ImgTrans(imgBuf);
StateTrans(leftPid.currentValue, rightPid.currentValue, 0, 0);
DelayMs(5);
}
}
MainProc
中总Init
汇集分Init
(由各模块提供接口),此处省略了作为小车实时控制的核心代码部分的定时器中断服务:
#include "MainProc.h"
#include "pit.h"
static void NVICInit(void);
static void TimerInit(void);
static void MainProc(void);
void MainInit() {
DelayInit();
NVICInit();
MotorInit();
EncoderInit();
SteerActuatorInit();
TimerInit();
DataCommInit();
ImgProcInit();
SpeedControlInit();
}
//下面省略...
其中有三个函数以static
声明为本地,确保只作为模块内部的工具使用,不被外部引用(封装黑盒)。
以电机模块为代表,旨在进一步地提供更好用的硬件接口,比如实现标准的输出(这是最简单的例子):
#include "Motor.h"
#include "ftm.h"
#define MOTOR_STOP MotorOut(0, 0)//意义不是特别大
void MotorInit() {
FTM_PWM_QuickInit(MOTOR_LEFT_FOR_MAP, kPWM_EdgeAligned, 10000);
FTM_PWM_QuickInit(MOTOR_LEFT_BAK_MAP, kPWM_EdgeAligned, 10000);
FTM_PWM_QuickInit(MOTOR_RIGHT_FOR_MAP, kPWM_EdgeAligned, 10000);
FTM_PWM_QuickInit(MOTOR_RIGHT_BAK_MAP, kPWM_EdgeAligned, 10000);
MOTOR_STOP;
}
void MotorOut(int16_t left, int16_t right) {
if(left > 10000) {
left = 10000;
} else if(left < -10000) {
left = -10000;
}
if(right > 10000) {
right = 10000;
} else if(right < -10000) {
right = -10000;
}
if(left>=0) {
FTM_PWM_ChangeDuty(MOTOR_LEFT_PORT, MOTOR_LEFT_FOR_CHL, left);
FTM_PWM_ChangeDuty(MOTOR_LEFT_PORT, MOTOR_LEFT_BAK_CHL, 0);
} else {
FTM_PWM_ChangeDuty(MOTOR_LEFT_PORT, MOTOR_LEFT_FOR_CHL, 0);
FTM_PWM_ChangeDuty(MOTOR_LEFT_PORT, MOTOR_LEFT_BAK_CHL, -left);
}
if(right>=0) {
FTM_PWM_ChangeDuty(MOTOR_RIGHT_PORT, MOTOR_RIGHT_FOR_CHL, right);
FTM_PWM_ChangeDuty(MOTOR_RIGHT_PORT, MOTOR_RIGHT_BAK_CHL, 0);
} else {
FTM_PWM_ChangeDuty(MOTOR_RIGHT_PORT, MOTOR_RIGHT_FOR_CHL, 0);
FTM_PWM_ChangeDuty(MOTOR_RIGHT_PORT, MOTOR_RIGHT_BAK_CHL, -right);
}
}
这里出现的一系列的地址映射宏(MOTOR_LEFT_FOR_MAP
, MOTOR_LEFT_PORT
等)在本人配置的PinMap
中以十分拙劣的方式实现。其作用是显而易见的——在增加可读性的同时,无论何时要对端口进行修改,也只须修改PinMap
一处(流传的代码很少有做到这一点的,故每次端口改动时都会花费极大量的时间在全面排查这种无意义的事上)。
顺带一提,关于宏的更多用法,感兴趣的童鞋可以了解下宏函数写法(ARM
体系中广泛用于位操作,写的极巧妙)、宏函数引发的不一致现象、do { ... } while(0)
的智慧、#if
、#ifdef
和#ifndef
条件编译等知识(最后这个广泛用于避免头文件的重复引用,使大工程的引用链问题容易解决)。此外,我偶尔也会使用#error
用于宏的一致性检验。
PinMap
示例如下(place-holder
表示硬件尚未实现或参数未整定):
#ifndef _PINMAP_H
#define _PINMAP_H
#ifdef MOTOR
//Motor
#define MOTOR_LEFT_PORT HW_FTM0
#define MOTOR_RIGHT_PORT HW_FTM0
#define MOTOR_LEFT_FOR_MAP FTM0_CH0_PC01
#define MOTOR_LEFT_BAK_MAP FTM0_CH1_PC02
#define MOTOR_RIGHT_FOR_MAP FTM0_CH2_PC03
#define MOTOR_RIGHT_BAK_MAP FTM0_CH3_PC04
#define MOTOR_LEFT_FOR_CHL HW_FTM_CH0
#define MOTOR_LEFT_BAK_CHL HW_FTM_CH1
#define MOTOR_RIGHT_FOR_CHL HW_FTM_CH2
#define MOTOR_RIGHT_BAK_CHL HW_FTM_CH3
#endif
#ifdef DATACOMM
//Data Communication
#define DATACOMM_IMG_TRANS_MAP UART4_RX_PC14_TX_PC15
#define DATACOMM_IMG_TRANS_BAUD 115200
#define DATACOMM_IMG_TRANS_CHL HW_UART4
#define DATACOMM_VISUALSCOPE_MAP UART2_RX_PD02_TX_PD03
#define DATACOMM_VISUALSCOPE_BAUD 115200
#define DATACOMM_VISUALSCOPE_CHL HW_UART2
#endif
#ifdef IMGPROC
//Camera
#define CAMERA_HREF_PORT HW_GPIOB
#define CAMERA_HREF_PIN 3
#define CAMERA_VSYN_PORT HW_GPIOC
#define CAMERA_VSYN_PIN 12
#define CAMERA_DATA_PORT HW_GPIOB
#define CAMERA_DATA_PIN 1
#define CAMERA_ODEV_PORT HW_GPIOB
#define CAMERA_ODEV_PIN 20
#define CAMERA_DATA_READ PBin(1)
#endif
#ifdef ENCODER
//Encoder
#define ENCODER_LEFT_PORT HW_FTM1
#define ENCODER_RIGHT_PORT HW_FTM2
#define ENCODER_LEFT_MAP FTM1_QD_PHA_PA08_PHB_PA09
#define ENCODER_RIGHT_MAP FTM2_QD_PHA_PA10_PHB_PA11
#endif
#ifdef STEER_ACTUATOR
//Steer Actuator
#define STEER_ACTUATOR_PORT HW_FTM1 //place-holder
#define STEER_ACTUATOR_MAP FTM1_CH0_PB00 //place-holder
#define STEER_ACTUATOR_CHL HW_FTM_CH0 //place-holder
#endif
//Check
#endif
类似的,我也配置了一个参数列表Param
,下文进一步说明:
#ifndef _PARAM_H
#define _PARAM_H
//NVIC
#define HREF_IRQ PORTB_IRQn
#define VSYN_IRQ PORTC_IRQn
#define TIMR_IRQ PIT0_IRQn
#ifdef MAINPROC
//PIT
#define PIT_CHL HW_PIT_CH0
#define PIT_PRD 5000
#endif
//Camera
#define IMG_ROW 50
#define IMG_COL 225
#ifdef IMGPROC
#define IMG_ABDN_ROW 19
#define IMG_ROW_INTV 5
#endif
#ifdef DATACOMM
#define IMG_WHITE 0xfe
#define IMG_BLACK 0x00
#define IMG_FRAME_FIN 0xff
#endif
#ifdef STEER_ACTUATOR
//Steer Actuator
#define STEER_ACTUATOR_RIGHT 447 //place-holder
#define STEER_ACTUATOR_MIDDLE 647
#define STEER_ACTUATOR_LEFT 847 //place-holder
#endif
//Speed Control
#ifdef SPEED_CONTROL
#define SPEED_CONTROL_PID_KP_L 30 //place-holder
#define SPEED_CONTROL_PID_KI_L 0.1 //place-holder
#define SPEED_CONTROL_PID_KD_L 0 //place-holder
#define SPEED_CONTROL_PID_KP_R 30 //place-holder
#define SPEED_CONTROL_PID_KI_R 0.1 //place-holder
#define SPEED_CONTROL_PID_KD_R 0 //place-holder
#define SPEED_CONTROL_LEFT_SPEED 50
#define SPEED_CONTROL_RIGHT_SPEED 50
#endif
#endif
下面以数据通信协议模块为例说明标志宏和常量宏的意义:
#include "DataComm.h"
#include "uart.h"
static uint16_t CrcCheck(uint8_t *Buf, uint8_t crcCnt);
void DataCommInit() {
UART_QuickInit(DATACOMM_IMG_TRANS_MAP, DATACOMM_IMG_TRANS_BAUD);
UART_QuickInit(DATACOMM_VISUALSCOPE_MAP, DATACOMM_VISUALSCOPE_BAUD);
}
void ImgTrans(byte imgBuf[IMG_ROW][IMG_COL]) {
int16_t i, j;
for(i = IMG_ROW - 1; i >= 0; i--) {
for(j = 0; j < IMG_COL; j++) {
if(imgBuf[i][j])
UART_WriteByte(DATACOMM_IMG_TRANS_CHL, IMG_WHITE);
else
UART_WriteByte(DATACOMM_IMG_TRANS_CHL, IMG_BLACK);
}
}
UART_WriteByte(DATACOMM_IMG_TRANS_CHL, IMG_FRAME_FIN);
}
uint16_t CrcCheck(uint8_t *Buf, uint8_t crcCnt) {
uint16_t crc = 0xffff;
uint8_t i, j;
for(i = 0; i < crcCnt; i++) {
crc ^= Buf[i];
for (j = 0; j < 8; j++) {
if (crc & 0x01)
crc = (crc >>1 ) ^ 0xa001;
else
crc = crc >> 1;
}
}
return(crc);
}
void StateTrans(float a, float b, float c, float d) {
float dataBuf[4] = {0};
uint32_t temp[4] = {0};
uint8_t frameBuf[10] = {0};
uint8_t i;
uint16_t crc = 0;
dataBuf[0] = a;
dataBuf[1] = b;
dataBuf[2] = c;
dataBuf[3] = d;
for(i = 0; i < 4; i++) {
temp[i] = (uint32_t)((int32_t)dataBuf[i]);
}
for(i = 0; i < 4; i++) {
frameBuf[i*2] = (uint8_t)(temp[i] % 256);
frameBuf[i*2+1] = (uint8_t)(temp[i] / 256);
}
crc = CrcCheck(frameBuf, 8);
frameBuf[8] = crc % 256;
frameBuf[9] = crc / 256;
for(i = 0; i < 10; i++) {
UART_WriteByte(DATACOMM_VISUALSCOPE_CHL, frameBuf[i]);
}
}
其中IMG_ROW
, IMG_COL
作为常量宏,IMG_WHITE
, IMG_BLACK
, IMG_FRAME_FIN
作为标志宏,其意义可由下图体现(上位机软件的截图):
图像宽度、图像高度、结束指令等参数通过这种方式耦合是更自然和规范的。
以图像分析处理模块说明变量的共享,imgProcFlag
即我此前提到的按位的标志,用于反映多种状态的复合,目前还没有做实现:
#include "ImgProc.h"
#include "gpio.h"
byte imgBuf[IMG_ROW][IMG_COL];
uint16_t imgProcFlag;
static uint8_t imgBufRow;
static uint8_t imgRealRow;
static void ImgProcHREF(uint32_t pinxArray);
static void ImgProcVSYN(uint32_t pinxArray);
void ImgProcInit(void) {
GPIO_QuickInit(CAMERA_HREF_PORT, CAMERA_HREF_PIN, kGPIO_Mode_IPU);
GPIO_QuickInit(CAMERA_VSYN_PORT, CAMERA_VSYN_PIN, kGPIO_Mode_IPU);
GPIO_CallbackInstall(CAMERA_HREF_PORT, ImgProcHREF);
GPIO_CallbackInstall(CAMERA_VSYN_PORT, ImgProcVSYN);
GPIO_ITDMAConfig(CAMERA_HREF_PORT, CAMERA_HREF_PIN, kGPIO_IT_RisingEdge, ENABLE);
GPIO_ITDMAConfig(CAMERA_VSYN_PORT, CAMERA_VSYN_PIN, kGPIO_IT_RisingEdge, ENABLE);
GPIO_QuickInit(CAMERA_DATA_PORT, CAMERA_DATA_PIN, kGPIO_Mode_IPU);
GPIO_QuickInit(CAMERA_ODEV_PORT, CAMERA_ODEV_PIN, kGPIO_Mode_IPU);
}
uint16_t ImgProcAnalyze(byte imgBuf[IMG_ROW][IMG_COL]) {
uint16_t ImgProcFlag;
//Do Something
return ImgProcFlag;
}
void ImgProcHREF(uint32_t pinxArray) {
int16_t i;
//if pinxArray & (1 << CAMERA_HREF_PIN) then
if(imgBufRow < IMG_ROW && imgRealRow > IMG_ABDN_ROW)
{
if(!(imgRealRow % IMG_ROW_INTV))
{
for(i = 0; i <= 195; i++);
for(i = IMG_COL - 1; i >= 0; i--)
imgBuf[imgBufRow][i] = CAMERA_DATA_READ;;;
imgBufRow++;
}
}
imgRealRow++;
}
void ImgProcVSYN(uint32_t pinxArray) {
//if pinxArray & (1 << CAMERA_VSYN_PIN) then
imgRealRow = 0;
imgBufRow = 0;
imgProcFlag = ImgProcAnalyze(imgBuf);
}
其中static
修饰的两个变量仅用于本地(声明为全局是有必要的,因其为两个中断服务所用),而未被修饰的两个还须由头文件extern
出去:
#ifndef _IMGPROC_H
#define _IMGPROC_H
#define IMGPROC
#include "root.h"
void ImgProcInit(void);
extern byte imgBuf[IMG_ROW][IMG_COL];
extern uint16_t imgProcFlag;
#endif
这样,其他文件就可以通过引用头文件来共享变量,无需大量的冗余extern
。函数的声明是默认为extern
的,即void ImgProcInit(void);
与extern void ImgProcInit(void);
等价。
extern
基本功能不作说明,请查阅课本,想必萌新们也碰到过变量未定义或重定义的问题吧。
关于extern
用法的更多信息,同样地,请活用搜索引擎(感兴趣的童鞋也可以了解下extern "C"
的意义,涉及一点点C++
知识,可以相应地用VS做一下dll dump
实验加深印象)。
然后是速度控制模块(PID
):
#include "SpeedControl.h"
#include "Motor.h"
PID leftPid, rightPid;
static int16_t SpeedControlPID(PID pid);
static void SpeedControlFilter(int16_t newValue, PID pid);
void SpeedControlInit() {
// uint8_t cnt;
// leftPid.currentValue = rightPid.currentValue = 0;
// for(cnt = 0; cnt < SPEED_BUF_SIZE; cnt++) {
// leftPid.valueBuf[cnt] = rightPid.valueBuf[cnt] = 0;
// }
// leftPid.cur = rightPid.cur = 0;
leftPid.targetValue = SPEED_CONTROL_LEFT_SPEED;
rightPid.targetValue = SPEED_CONTROL_RIGHT_SPEED;
leftPid.kp = SPEED_CONTROL_PID_KP_L;
leftPid.ki = SPEED_CONTROL_PID_KI_L;
leftPid.kd = SPEED_CONTROL_PID_KD_L;
rightPid.kp = SPEED_CONTROL_PID_KP_R;
rightPid.ki = SPEED_CONTROL_PID_KI_R;
rightPid.kd = SPEED_CONTROL_PID_KD_R;
// leftPid.lastError = rightPid.lastError = 0;
// leftPid.prevError = rightPid.prevError = 0;
// leftPid.output = rightPid.output = 0;
}
void SpeedControlProc(int16_t leftSpeed, int16_t rightSpeed, uint16_t imgProcFlag) {
//Dispatch and Translate the 'imgProcFlag'
SpeedControlFilter(leftSpeed, leftPid);
SpeedControlFilter(rightSpeed, rightPid);
MotorOut(SpeedControlPID(leftPid), SpeedControlPID(rightPid));
}
int16_t SpeedControlPID(PID pid)
{
int16_t error;
double pValue, iValue, dValue;
error = pid.targetValue - pid.currentValue;
pValue = pid.kp * (error - pid.lastError);
iValue = pid.ki * error;
dValue = pid.kd * (error - 2 * pid.lastError + pid.prevError);
pid.output += pValue + iValue + dValue;
pid.prevError = pid.lastError;
pid.lastError = error;
return (int16_t)pid.output;
}
void SpeedControlFilter(int16_t newValue, PID pid) {
uint8_t cnt;
pid.valueBuf[pid.cursor] = newValue;
pid.cursor++;
if(pid.cursor / SPEED_BUF_SIZE) {
pid.cursor = 0;
}
pid.currentValue = pid.valueBuf[0];
for(cnt = 1; cnt < SPEED_BUF_SIZE; cnt++) {
pid.currentValue += pid.valueBuf[cnt];
pid.currentValue /= 2;
}
}
把信息封装在了PID
结构体中,而提供给MainProc
的SpeedControlProc
接口则只作分开的参数填入,避免结构体过度的外泄(其实可以做到完全不外泄的,正在考虑其优劣)。
这里的PID
结构体在方向控制环也有用到,其原型定义在TypeDef
头文件中:
#ifndef _TYPEDEF_H
#define _TYPEDEF_H
#include "common.h"
#define SPEED_BUF_SIZE 5
typedef struct _PID {
int16_t currentValue;
int16_t valueBuf[SPEED_BUF_SIZE];
uint8_t cursor;
int16_t targetValue;
double kp;
double ki;
double kd;
int16_t lastError;
int16_t prevError;
double output;
}
PID;
typedef uint8_t byte;
#endif
此外,因为目前还没有正式投入调试,额外的编译配置(模块开关)头文件Config
未被广泛使用。这个功能在去年的代码中就有体现了(那时还比较嫩):
/* 宏开关 */
#define AC
#define VC
#define DC
#define xOUT_JUDGE
#define xSINGLE_VC
#define DYNAMIC_DC_PID
#define xFIX
#define RS_JUDGE
#define xSLOW_DOWN
#define x__DEBUG__
用于条件编译,如:
#define MAINPROC_GLOBALS
#include "MainProc.h"
#ifdef RS_JUDGE
static bool startFlag;
static bool start;
#endif
/**
* @brief 标准总处理, 集中角度环、速度环和方向环, 给予电机标准输出
*/
void Main_Process(void)
{
#ifndef SINGLE_VC
dirAngleSpeed = DirGyro_Get();
#ifdef SLOW_DOWN
if(dirAngleSpeed > TURN_FLAG || dirAngleSpeed < -TURN_FLAG) {
if(VC_Set > VC_Min) {
VC_Set--;
} else if(VC_Set < VC_Min) {
VC_Set++;
}
} else {
if(VC_Set < VC_Max) {
VC_Set++;
} else if(VC_Set > VC_Max) {
VC_Set--;
}
}
#endif
#if defined(VC) || defined(DC)
speed = Encoder_Get();
#endif
#ifdef AC
AC_Out = Angle_Process();
#endif
#ifdef VC
VC_Out = Velocity_Process(speed);
#endif
#ifdef DC
DC_Out = Direction_Process(speed);
#endif
#ifdef RS_JUDGE
if(!start) {
if(ReedSwitch_Get()) {
startFlag = true;
} else if(!ReedSwitch_Get() && startFlag) {
start = true;
}
} else {
if(ReedSwitch_Get()) {
out = true;
}
}
#endif
#if defined(OUT_JUDGE) || defined(RS_JUDGE)
if(out)
{
MOTOR_STOP();
}
else
{
#endif
Left_Out = AC_Out - VC_Out - DC_Out;
Right_Out = AC_Out - VC_Out + DC_Out;
Motor_Out(Left_Out, Right_Out);
#if defined(OUT_JUDGE) || defined(RS_JUDGE)
}
#endif
#else /* SINGLE_VC */
speed = Encoder_Get();
VC_Out = Velocity_PID(VC_Set, speed);
Motor_Out(VC_Out, VC_Out);
#endif /* SINGLE_VC */
}
此外,去年对变量共享使用了从操作系统构建中学来的“黑科技”(上面那个显眼的MAINPROC_GLOBALS
意义所在):
#ifndef __MAINPROC_H
#define __MAINPROC_H
#include "include.h"
#include "Angle.h"
#include "Velocity.h"
#include "Direction.h"
#include "Motor.h"
#ifdef MAINPROC_GLOBALS
#define MAINPROC_EXT
#else
#define MAINPROC_EXT extern
#endif
#define ReedSwitch_Get() !GPIO_ReadBit(RS_PORT, RS_PIN)
MAINPROC_EXT int32_t AC_Out;
MAINPROC_EXT int32_t VC_Out;
MAINPROC_EXT int32_t DC_Out;
MAINPROC_EXT int32_t Left_Out, Right_Out;
#if defined(SINGLE_VC) || defined(VC) || defined(DC)
MAINPROC_EXT int32_t speed;
MAINPROC_EXT float distance;
#endif
void Main_Process(void);
#endif
具体来说,是从uC/OS-II和III学来的:
#ifdef OS_GLOBALS
#define OS_EXT
#else
#define OS_EXT extern
#endif
即OS_GLOBALS
被定义时,OS_EXT
修饰变量为定义,否则为引用。在相应的实现文件中,引用头文件之前定义此宏,也可实现变量共享。个人认为我在代码中对此的模仿显得很拙劣,并没有实现多大的优势,故后来不再用了。
成型的模块开关(条件编译)可以是这样(我们做的一个仪器的代码的一部分):
#include "includes.h"
#include "SoundSampling.h"
#include "ErrorProc.h"
#ifdef DEBUG
char strBuf[50];//DEBUG路字符缓冲区
#endif
//static int32_t getSampleTime;
static bool getReady;//复位完成标志, 为true时正式开始采样, 否则等待复位
/**
* @brief 定时器中断服务函数, 捕捉reverse消息, 并向下发送一个reversePush消息
*/
void Timer_Int_Service() {
#ifdef OLED
int32_t cnt;
//周期刷新屏幕
if(!(time % OLED_FRESH_PERIOD)) {
OLED_CLS();
for(cnt = 0; cnt < 8; cnt++) {
OLED_P6x8Str(0, cnt, (uint8_t*)oledBuf[cnt]);//把缓冲区字符串都打印在屏幕上
}
}
#endif
time += TIMER_CNT / 1000;//计时, 单位ms
#ifdef INFRARED2
//准备阶段
if(!getReady) {
time += TIMER_CNT / 1000;
//周期红外测距, 此处为发送命令, 捕捉在其他地方完成
if(!(time % INFRARED_PERIOD)) {
periodGetInfaredData = true;//区分于其他类型红外测距的标志
Infrared_Get(DISTANCE);
}
}
//正式采样过程
if(!finish && getReady) {
#else
if(!finish) {
#endif
// if(getSample) {
// getSample = false;
// getSampleTime = time;
// }
// if(!(time % INFRARED_PERIOD) && time - getSampleTime > INFRARED_PERIOD) {
// periodGetInfaredData = true;
// Infrared_Get(DISTANCE);
// }
//周期推杆折返
if(!(time % ONCE_PERIOD)) {
PIT_ITDMAConfig(HW_PIT_CH0, kPIT_IT_TOF, false);
Motor_Control(STOP);
DelayMs(500);
up = !up;//折返
Motor_Execute();
reverse = true;//向下投递一个reverse消息
PIT_ITDMAConfig(HW_PIT_CH0, kPIT_IT_TOF, true);
}
//捕捉reverse消息, 向下投递一个reversePush消息
if(reverse) {
time = 0;//时间复位, 方便soundSampling层分辨参考波峰
// getSampleTime = 0;
reverse = false;//捕捉reverse消息
reversePush = true;//向下投递一个reversePush消息
}
Sound_Sampling_Proc();//采样过程
}
}
/**
* @brief 定时器初始化, 周期为TIMER_CNT, 默认为5000(us)
*/
void Timer_Init() {
PIT_QuickInit(HW_PIT_CH0, TIMER_CNT);
PIT_CallbackInstall(HW_PIT_CH0, Timer_Int_Service);
PIT_ITDMAConfig(HW_PIT_CH0, kPIT_IT_TOF, true);
}
/**
* @brief 多路UART中的DEBUG路初始化
*/
void Debug_Init() {
UART_QuickInit(UART_DEBUG_MAP, DEBUG_BAUDRATE);
}
/**
* @brief 获得推杆起始位置, 并根据其距离管顶或管底的远近来决定其复位到顶部还是底部
*/
static void Ready_Position_Get() {
bool success = false;
do {
Infrared_Get(DISTANCE);
while(!getInfraredData);
getInfraredData = false;
Infrared_Get(DISTANCE);
while(!getInfraredData);
getInfraredData = false;
if(!getWrongData) {
if(infraredData > MIDDLE) {
up = false;
} else {
up = true;
}
success = true;
}
//这个傻逼模块第一次测距一定是错误数据32, 所以测两次
// sprintf(strBuf, "%d %.1f %d\r\n", up, infraredData, getWrongData);
// UART_printf(UART_DEBUG_CHL, strBuf);
} while(!success);
}
/**
* @brief 系统总初始化, main唯一要调用的初始化函数
*/
void System_Init() {
DelayInit();//延时初始化
// Key_Init();//按键初始化
// Key_Scan();//按键读取
#ifdef OLED
Oled_Init();//OLED初始化
#endif
#ifdef VISUAL_SCOPE
VisualScope_Init();//上位机示波器初始化
#endif
#ifdef DEBUG
Debug_Init();//调试用串口初始化
#endif
#ifdef VOICE
Voice_Init();//播放模块初始化
#endif
#ifdef DS18B20
DS18B20_Init();//DS18B20测温初始化
#endif
Sound_Sampling_Init();//咪头初始化
#if defined(INFRARED) || defined(INFRARED2)
Infrared_Init();//红外测距初始化
#ifdef INFRARED2
Ready_Position_Get();//起始位置确定
#endif
#endif
Motor_Init(); //电机初始化
Motor_Execute(); //电机运行
Timer_Init(); //定时器初始化
}
/**
* @brief 用户程序入口
*/
int main() {
System_Init();
while(1) {
#ifdef INFRARED2
//复位过程中, 进行周期红外测距, 当推杆到达管顶或管底时, 完成折返, 正式开始采样, 此处为捕捉红外数据
if(!getReady && getInfraredData && periodGetInfaredData) {
getInfraredData = false;
periodGetInfaredData = false;
// sprintf(strBuf, "%d %.1f %d\r\n", up, infraredData, getWrongData);
// UART_printf(UART_DEBUG_CHL, strBuf);
if(!getWrongData) {
if((infraredData > BOTTOM && !up) || (infraredData < TOP && up)) {
PIT_ITDMAConfig(HW_PIT_CH0, kPIT_IT_TOF, false);//关定时器
// sprintf(strBuf, "%d\r\n", up);
// UART_printf(UART_DEBUG_CHL, strBuf);
Motor_Control(STOP);
DelayMs(500);
up = !up;//折返
// sprintf(strBuf, "%d\r\n", up);
// UART_printf(UART_DEBUG_CHL, strBuf);
Motor_Execute();
time = 0;//时间复位, 从正式采样开始计时
getReady = true;//开始采样
PIT_ITDMAConfig(HW_PIT_CH0, kPIT_IT_TOF, true);//开定时器
}
} else {
getWrongData = false;
}
}
#endif
//结束采样, 做数据处理
if(finish) {
PIT_ITDMAConfig(HW_PIT_CH0, kPIT_IT_TOF, false);
Measure_Finish(sampleArray);//数据处理
while(1);//结束
}
}
//never get here
}
4. 回答疑问
讲一下命令解释器Command Interpreter
的写法,因为调试的时候很常用。这也是下一个我要给框架提供的接口。
下面这一块代码和上一块同属一个工程,和其他模块配合,形成了定时/条件发送(查询),延迟交付的技术(有点像TCP/IP流水线
),这里不做赘述,重点放在命令解释器:
#include "Infrared.h"
/*********************************************************
*********激光测距模块*************************************
********指令 ‘O’----打开激光******************************
********指令 ‘C’----关闭激光******************************
********指令 ‘D’----测量距离******************************
********指令 ‘S’----查看模块状态(温度和电压)**************
**********************************************************/
static bool sendQuest;//命令执行过程中标志, 为true时表示命令尚在处理中, 否则是未发送(接收)命令或已处理完成
static char disBuf[20]={0};//距离结果字符缓冲区
static char tempBuf[30]={0};//温度结果字符缓冲区
static char *ptr;//字符指针
float infraredData; //激光测距数据
float tempData; //温度数据数据
INFRARED_DATA_TYPE infraredDataType;//获取数据类型
bool getInfraredData;//结果字符串已接收处理完成标志, 为true时表示结果已准备就绪, 否则要继续等待接收和处理
bool getWrongData;//获得错误数据标志, 为true时接收数据为模块内置的Error码(此处简化, 统一作为同一Error处理)
double soundVelocity;//声速
#if defined(INFRARED) || defined(INFRARED2)
/**
* @brief 根据温度计算声速
* @retval 声速, 单位是m/s
*/
static float Sound_Velocity_Calc() {
Infrared_Get(TEMPERATURE);//获取温度
while(!getInfraredData);
getInfraredData = false;
// sprintf(strBuf, "%f %d\r\n", tempData, getWrongData);
// UART_printf(UART_DEBUG_CHL, strBuf);
//傻逼模块
//连续测两次就死了
return 331.45 + 0.61 * tempData; //物理学公式
}
#endif
/**
* @brief 串口中断回调函数, 接收红外模块的反馈和结果
* @param[in] byte 接收到的字节
*/
static void Infrared_Data_Get(uint16_t byte) {
// UART_WriteByte(UART_DEBUG_CHL, byte);
//解析数据
if(sendQuest) {
PIT_ITDMAConfig(HW_PIT_CH0, kPIT_IT_TOF, false);//关定时器, 重要!因为这里决不能被定时器中断打断
//根据数据类型解析
switch(infraredDataType) {
//接收的是距离数据
//解析前是字符串, 形式为 D: xxxm, xxx
//m前的是整数部分, 后则是小数部分
//当出现异常时形式则为 Erxx (xx是错误代码)
//解析后转为float, 形式为 xxx.xxx(ms)
//存入infraredData中
//异常时另做处理
case DISTANCE:
if(byte >= '0' && byte <= '9') {
*ptr = byte;
ptr++; //不考虑溢出
} else if(byte == 'm') {
*ptr = '.';
ptr++;
} else if(byte == '\n') {
*ptr = '\0';
infraredData = atof(disBuf);//stdlib的库函数, 将字符串转为浮点数
sendQuest = false;
getInfraredData = true;//接收和处理完成
PIT_ITDMAConfig(HW_PIT_CH0, kPIT_IT_TOF, true);//开定时器
} else if(byte == 'E') {
sendQuest = false;
getInfraredData = true;
getWrongData = true;//提示数据错误
PIT_ITDMAConfig(HW_PIT_CH0, kPIT_IT_TOF, true);
}
break;
//接收的是温度数据
//解析前是字符串, 形式为 S: xxx.xxx`C
//解析后转为float, 形式为 xxx.xxx(`C)
//存入tempData中
//无异常情况
case TEMPERATURE:
if((byte >= '0' && byte <= '9') || byte == '.') {
*ptr = byte;
ptr++; //不考虑溢出
} else if(byte == 'C') {
*ptr = '\0';
tempData = atof(tempBuf);
sendQuest = false;
getInfraredData = true;
PIT_ITDMAConfig(HW_PIT_CH0, kPIT_IT_TOF, true);
}
break;
}
}
}
/**
* @brief 红外测距初始化
*/
void Infrared_Init(void) {
UART_QuickInit(UART_INFRARED_MAP, INFRARED_BAUDRATE);//模块固定波特率19200
UART_CallbackRxInstall(UART_INFRARED_CHL, Infrared_Data_Get);
UART_ITDMAConfig(UART_INFRARED_CHL, kUART_IT_Rx, true);
UART_WriteByte(UART_INFRARED_CHL, 'O');//打开激光
DelayMs(1000);//忽略模块反馈
soundVelocity = Sound_Velocity_Calc() * 1000;//根据温度计算声速
}
/**
* @brief 发送测量命令, 延迟接收其结果
* @param[in] choice 要测量的数据类型(距离或温度)
*/
void Infrared_Get(INFRARED_DATA_TYPE choice) {
infraredDataType = choice;
switch(choice) {
case DISTANCE:
UART_WriteByte(UART_INFRARED_CHL, 'D');//距离测量指令
ptr = disBuf; //选择接收地址
break;
case TEMPERATURE:
UART_WriteByte(UART_INFRARED_CHL, 'S');//温度测量指令
ptr = tempBuf;
break;
}
sendQuest = true;//开始接收处理
}
这里是根据预定格式按特定字符解析字符串,而不是做串的匹配,两者各有长处。在此处,我可以确定模块的response
一定是valid
的,而没有和人类交互时的不确定性,故使用按特定字符解析性能更好。串匹配我写过一个不是很完善的例子(应付作业的):
#include <rtx51tny.h>
#include <reg51.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
//居然没有stdint和stdbool标准库
typedef unsigned char uint8_t;
sbit LED1=P2^0;
sbit LED2=P2^1;
sbit LED3=P2^2;
sbit LED4=P2^3;
#define BUF_SIZE 20u
#define COM_TURN_ON "turn on"
#define COM_TURN_OFF "turn off"
#define COM_INVALID "invalid command"
#define RSP_TURN_ON_OFF_HEAD "led"
#define RSP_TURN_ON_TAIL "on"
#define RSP_TURN_OFF_TAIL "off"
#define COD_TURN_ON_LED1 0x81u
#define COD_TURN_ON_LED2 0x82u
#define COD_TURN_ON_LED3 0x83u
#define COD_TURN_ON_LED4 0x84u
#define COD_TURN_OFF_LED1 0x01u
#define COD_TURN_OFF_LED2 0x02u
#define COD_TURN_OFF_LED3 0x03u
#define COD_TURN_OFF_LED4 0x04u
char buf[BUF_SIZE];
char* bufptr;
uint8_t blink = 0xf;
void UartInit() {
SCON=0X50;
TMOD=0X20;
PCON=0X80;
TH1=0XF3; //波特率4800
TL1=0XF3;
ES=1;
EA=1;
TR1=1;
}
void SysInit() _task_ 0 {
UartInit();
os_create_task(1);
os_create_task(2);
os_create_task(3);
os_create_task(4);
os_create_task(5);
os_delete_task(0);
}
void UartReceive() interrupt 4 {
char recv;
RI = 0;
recv = SBUF;
*bufptr = recv;
if(bufptr - buf < BUF_SIZE - 1) {
bufptr++;
}
if(recv >= '1' && recv <= '4') {
*bufptr = 0;
bufptr = buf;
os_send_signal(5);
}
}
void Blink1() _task_ 1 {
while(1) {
if(blink & 0x01) {
LED1=!LED1;
os_wait(K_TMO, 20, 0);
} else {
LED1=1;
}
}
}
void Blink2() _task_ 2 {
while(1) {
if(blink & 0x02) {
LED2=!LED2;
os_wait(K_TMO, 200, 0);
} else {
LED2=1;
}
}
}
void Blink3() _task_ 3 {
while(1) {
if(blink & 0x04) {
LED3=!LED3;
os_wait(K_TMO, 400, 0);
} else {
LED3=1;
}
}
}
void Blink4() _task_ 4 {
while(1) {
if(blink & 0x08) {
LED4=!LED4;
os_wait(K_TMO, 800, 0);
} else {
LED4=1;
}
}
}
void UartSend(char rsp[]) {
uint8_t cnt;
ES = 0;
for(cnt = 0; cnt < BUF_SIZE; cnt++) {
SBUF = rsp[cnt];
while(!TI);
TI=0;
if(!rsp[cnt]) {
break;
}
}
ES=1;
}
uint8_t CommandTranslate(char com[]) {
uint8_t cnt;
char *ptr;
uint8_t turnon;
if(strstr(com, COM_TURN_ON)) {
turnon = 1;
} else if(strstr(com, COM_TURN_OFF)) {
turnon = 0;
} else {
return 0x0;
}
ptr = com;
cnt = 0;
while(*ptr && !(*ptr >= '1' && *ptr <= '4') && cnt < BUF_SIZE) {
ptr++;
cnt++;
}
if(*ptr && cnt < BUF_SIZE) {
if(turnon) {
return 0x1 << 7 | atoi(ptr);
} else {
return atoi(ptr);
}
} else {
return 0x0;
}
}
void TaskControl() _task_ 5 {
uint8_t validComm = 1;
uint8_t mux;
while(1)
{
os_wait(K_SIG, 0, 0);
mux = CommandTranslate(buf);
switch(mux) {
case COD_TURN_ON_LED1:
blink |= 0x01;
break;
case COD_TURN_ON_LED2:
blink |= 0x02;
break;
case COD_TURN_ON_LED3:
blink |= 0x04;
break;
case COD_TURN_ON_LED4:
blink |= 0x08;
break;
case COD_TURN_OFF_LED1:
blink &= ~0x01;
break;
case COD_TURN_OFF_LED2:
blink &= ~0x02;
break;
case COD_TURN_OFF_LED3:
blink &= ~0x04;
break;
case COD_TURN_OFF_LED4:
blink &= ~0x08;
break;
default:
validComm = 0;
break;
}
if(validComm) {
if(mux&0x80) {
mux &= ~0x80;
sprintf(buf, "%s%u%s\n", RSP_TURN_ON_OFF_HEAD, mux, RSP_TURN_ON_TAIL);
} else {
sprintf(buf, "%s%u%s\n", RSP_TURN_ON_OFF_HEAD, mux, RSP_TURN_OFF_TAIL);
}
} else {
sprintf(buf, "%s\n", COM_INVALID);
}
UartSend(buf);
validComm = 1;
}
}
值得一提的是,我使用了COD_TURN_ON_LED1
等宏,将指令以按位的消息的形式呈现,于是就可以switch
了。此外还做了必要的边界检测等,算是比较健壮。
上面提到的两个例子,都以特定的某类字符作为结束符了。那么有没有不以什么字符作为结束符,也可以使MCU
知道字符串结束的方法呢?加个定时器就料理了——超时即结束。
5. 结语
第一次写博文,大家多多包含。以后会把以前在工作室里积累的干货逐渐弄上来讲讲,也能提高下自己的水平。但是之后几篇,不出意外的话,应该是《计算机网络 自顶向下方法》这本书的读书笔记(其实那个的稿子写得比这个还要早,已经写了有一半了,而因为种种原因先发了这个)。那么,就这样。