这篇文章写给M3的工作者们,我会介绍一些不那么死板的用法以及更优秀的编程思想,看完这篇文章你的水平会立刻增加一个百分点。
前言
子曾经曰“M3学习7天就够”,7天以后从事M3开发的人员编写代码的能力都是差不多的。是的,不用怀疑,首席/高级/资深工程师们的代码你也能写出。经验和对项目的认真程度往往决定你能在一家公司走多远。本文的目的是给你介绍一些M3上俏皮的代码和非常优秀的编程模型,帮助你在M3或者其他平台上少走弯路变得更强。
跳动的代码
谁说不能用goto
goto会让程序跳转逻辑变得复杂,所以许多c程序开发人员都挺忌讳这个。一般来说,goto可以用在同一函数以内的错误处理和循环和循环之间跳转。
这里我举两个例子说明goto用法。第一个例子中我将func1和func2放置在了两个不同的死循环中,一般来说执行func1后很难执行func2,这里用了goto,当something1发生后强制跳转到另一个循环中从而执行func2,同理,当something2发生后返回执行func2。第二个例子中,使用了goto的跳转特性,当发生错误后在error标签后进行错误处理,比如清理错误影响。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用goto控制两个不同循环中func1和func2的执行
void task(void)
{
lab1:
while(1){
func1();
if(something1 == happened)
goto lab2;
}
lab2:
while(1){
func2();
if(something2 == happened)
goto lab1;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 使用goto进行错误处理
void function(void)
{
if(something1 != happened)
{
func1();
if(something2 == happened)
goto error;
}
else
goto error;
func2();
return;
error:
clean();
return;
}
让switch选择一个范围
switch函数的case确实不支持表达式,但是可以转换为在switch的参数里填入bool类型结果的表达式,从而使switch可以选择一个范围。同时我发现对于default,大部分人还是会选择另起default…break的结构,其实没有必要,直接和目标case连写即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 选择n在6到9执行func1函数
void function1(void)
{
switch((n>=6)&&(n<=9))
{
case true:
func1();
break;
case false:
func2();
break;
}
}
// 摆放default的位置
void function2(void)
{
switch(i)
{
default:
case 0:
func3();
break;
case 1:
func4();
break;
case 2:
func5();
break;
}
}
static让队列中的函数报数
哈喽,func52函数你在队列中的序号是多少?我的序号是20。
你曾经需要手动确定函数在队列中的序号,然后执行该函数,使用static函数内部静态变量帮助你快速获取序号。
1
2
3
4
5
6
7
8
9
10
int function(int arg)
{
static int index = RET_NONE;
if(arg == SAVE)
index = reg_index; // 保存函数注册进队列的时候的序号,function(SAVE)
else if(arg == LOAD)
return index; // 读取函数在队列中的序号,function(LOAD)
some_func(); // 正常函数代码
return RET_NONE;
}
可变长参数玩转C参数
有时我们想输入多个参数,比如取得多个参数中的最小值。
像下面的程序,只需输入function(2,0,5,END)
就可以获取到最小值0。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define END -65536
int function(int arg,...)
{
int *ap;
ap = &arg;
{ // 此处的{}新建一个区域方便声明变量
int min = *ap;
while(*(++ap) != END)
{
if(min > *ap)
min = *ap;
}
return min;
}
}
除上面介绍的c使用方法外,还有很多也能帮助我们巧妙的解决问题或者让程序可读性更高。比如宏定义的##连接符,前后台框架等。下面我就不继续介绍方法了,谈一下流水线型编程思想。
思想
说起流水线型程序不得不谈下if叠加形式的程序,两段功能完全相同的程序给你展示两者的区别。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// if叠加型程序
void function1()
{
if(condition1 == happened)
{
func1();
if(condition2 == happened)
{
func2();
}
else
error2();
}
else
error1();
}
// 流水线型程序
void function2()
{
if(condition1 != happened)
{
error1();
return;
}
func1();
if(condition2 != happened)
{
error2();
return;
}
func2();
}
相比较而言,if嵌套程序逻辑更加明显,但当代码存在复杂的跳转关系时,代码将庞大且不易懂。流水线程序并非不使用if嵌套,而是尽可能减少if之间的耦合,便于维护庞大代码情况下的逻辑处理,我认为这是更加优秀的现代化编程方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 比如通讯协议流水线化
// 示例代码展示了流水线型编程在应用上的可行性
UsartRecBuf[RecIndex++] = Temp;
if(RecIndex >= MaxLength)
{
RecIndex = 0;
}
if(UsartRecBuf[0] != 1)
{
RecIndex = 0;
return;
}
if((UsartRecBuf[1] != 0x03)&&(UsartRecBuf[1] != 0x04))
{
RecIndex = 0;
return;
}
if(RecIndex == (UsartRecBuf[2] + 5))
{
//something
}
优化
你想走的更远,那你一定要看看优化篇。在这个篇章中主要提到库函数可重入,驱动模块化设计。
关于库函数的要求
库函数需要可重入设计,类似于程序的多开。
1
2
3
4
int min(int a,int b)
{
return a<b?a:b;
}
在上面的例子中支持执行min(1,3)的时候对min(2,4)无影响。这就是可重入。
如果func1函数中声明了静态局部变量且该变量被无法代替的使用,那么中间该静态变量将被赋予一个值,这样多处同时执行func1时由于中间静态变量的变化将使func1输出可能不是预期值。
我整理了几点关于库函数可重入的必要条件:
- 不包含被使用的全局变量(非传参,且发挥了无法用临时变量取代的作用)
- 不包含被使用的静态局部变量(非传参,且发挥了无法用临时变量取代的作用)
- 有返回值
如果函数不满足上述几条,则不应该归为库函数,而属于应用函数。
驱动模块化设计
什么是模块化?去掉模块后不影响不基于该模块的其他部分这就是模块化。从这个意义上来讲,函数的模块化并不能算模块化设计,属于可重入优化。驱动模块化设计是指将一个文件处理为一个模块的一种设计方法,有如下特点:
- 对全局变量(不加static的)苛刻的使用,甚至不使用
- 对文件内全局变量和函数需要加static修饰,不同模块里的文件内全局变量可重名
- 将控制函数指针和接口变量值存入结构体变量
- 将该结构体指针传出(一般有两种方法:将结构体外部声明;声明一个包括外部的全局函数将结构体指针传出)
一般来说一个*.c文件作为一个驱动模块是最合适的。
设计步骤如下:
- 确定模块结构体的接口:哪些控制函数、接口变量
- 编写模块结构体,并赋初值,缺省值一般为0
- 编写接口控制函数,一般可以简化为probe、write、read、ioctl
- 编写外部传出函数,主要传出结构体地址以及调用probe函数做初始化操作
其他优化
针对有外部存储的产品,一定要建立一个外部存储文档单独维护。主要列举外部存储中的变量名和恢复出厂后默认值。变量地址如结构体的存储在外部基地址、结构体内部元素的偏移地址可选。