M3的终极,思想的开端

干货放送,给你介绍俏皮的代码和优秀的编程模型

Posted by Jerry Chen on June 19, 2019

这篇文章写给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文件作为一个驱动模块是最合适的。

设计步骤如下:

  1. 确定模块结构体的接口:哪些控制函数、接口变量
  2. 编写模块结构体,并赋初值,缺省值一般为0
  3. 编写接口控制函数,一般可以简化为probe、write、read、ioctl
  4. 编写外部传出函数,主要传出结构体地址以及调用probe函数做初始化操作

其他优化

针对有外部存储的产品,一定要建立一个外部存储文档单独维护。主要列举外部存储中的变量名和恢复出厂后默认值。变量地址如结构体的存储在外部基地址、结构体内部元素的偏移地址可选。