单片机C语言骚 *** 作

单片机C语言骚 *** 作,第1张

本人的分享均来自于实际设计过程中的感悟 不能保证分享成果的正确性,如有错误,请各路大神指出,我会虚心学习,感谢!!!

一、普遍的驱动初始化方法:

         大家在编写单片机程序的时候,不知道有没有一种感觉,就是在写驱动程序的时候,很多时候,每一个驱动的初始化函数,都需要通过头文件extern导出。然后再到main.c文件中的main函数中去调用该初始化函数。如此一来,mian函数中全都是初始化函数,并且每个驱动程序都需要单独导出初始化函数提供给mian函数调用。但是实际上mian函数其实并不关心初始化调用的是哪些函数,它只需要在程序运行时运行一遍驱动的初始化 *** 作即可。

       

如图所示的工程中,有三个文件:

dev_led.c驱动源文件

dev_led.h驱动头文件

main.c主程序源文件

通常情况下如果想要初始化dev_led驱动,需要哪些步骤呢?下面我们来一一分解:

1、第一步:在dev_led.c文件中编写好dev_led驱动初始化代码。

2、第二步:在dev_led.h文件中通过extern 导出led_init函数,以便于其他文件使用。

3、第三步:在KEIL中设置dev_led.h的头文件包含路径,否则会找不到该头文件。

4、第四步:在mian.c头文件中包含dev_led.h头文件。

5、第五步:在main函数中调用led_init函数,完成dev_led驱动的初始化。

        这些 *** 作,大部分驱动都是如此,这就引发了我的一些思考。有没有什么办法,不用通过头文件的导出导入,就可以实现驱动程序的初始化吗?如此一来,即解决了头文件处理繁琐的问题,也解决的驱动模块和逻辑代码间的耦合问题,这样该多好啊。

二、自动初始化的原理

        一顿google,百度之后,我发现早就有人想过这个问题(具体谁发明的不知),并且解决了这个问题,利用最广泛的地方就是Linux的驱动初始化中。如果学过Linux底层原理的朋友应该知道,Linux驱动文件是通过moudle_init宏定义实现驱动的初始化和一系列的注册 *** 作的,module_exit宏定义实现驱动的卸载等 *** 作。

        在使用moudle_init宏加载初始化函数的时候,这个初始化函数,好像并没有说通过头文件导出,然后在main函数中调用,但是为什么在系统加载的过程中,这个初始化函数就被调用了呢,这里面的玄机就在moudle_init这个宏定义中,由于这篇文章篇幅有限,我们不讨论Linux中是如何实现这个机制的,网上也有很多这个宏定义的解析,都写得很详细。我们直接看看如何在单片机环境中实现这个机制,以便于我们形成自己的程序组织架构。这里我们只讨论在KEIL环境下,其他环境是否兼容,可能需要编译器的支持,请大家自己尝试。

        根据Linux中实现的原理,其实就是通过编译器特有的预编译指令,使得初始化函数的指针包含在一个特定的程序段中,并且这个函数按照一定的规则排序,注意,这里函数的指针,是在预编译阶段就确定下来,要放到指定的地方去的。

三、KEIL中实现自动初始化知识点

        首先我们要知道几个预编译指令的功能和使用场景.

1、__attribute__ 

我们只需要知道__attribute__ 能够指定函数的特性即可。

参考文章:【C语言】__attribute__使用_叮咚咕噜的博客-CSDN博客_attribute c语言

2、used

__attribute__((used))

        这条指令可以使得指定的函数,在编译时,不会被编译器优化掉,因为,我们在使用初始化函数的时候,可能不会被任何地方显示的调用,所以不加这个指令,可能编译器会认为该函数没有被使用,于是就自动优化掉了该函数,导致错误发生。

下面我们通过一个列子看看,是不是真的是这样,代码如下所示:

__attribute__((used)) void FUNC1(void)
{
	
}

void FUNC2(void)
{
	
}

int main(void)
{
	while(1)
	{
		
	}
}

        这段代码中,FUNC1被添加了used修饰,而FUNC2没有加used修饰,并且这两个函数都没有被任何地方显示地调用,编译之后,我们双击keil的工程,即可看到工程的map文件,参考下图,其中就有函数的段分配情况,可以看到,FUNC1被编译成功了,而FUNC2并没有找到,这证明used修饰后的函数,不会被编译器优化掉

3、section

__attribute__ ((section ("abc")))

        这条指令可以使得指定的函数,在编译时,存放在指定名称的段中。由于我们要隐式的调用初始化函数,所以一定要在编译后就知道函数的位置,使用该特性就可以实现这个功能。

        下面我们看看是不是真的能实现这些功能,代码如下:

 __attribute__ ((section ("abc"))) __attribute__((used)) void FUNC1(void)
{
	
}

void FUNC2(void)
{
	
}


int main(void)
{
	while(1)
	{
		
	}
}

其中FUNC1被放入了代码段abc中,我们双击工程查看map文件,可以找到FUNC1函数,确实就是在abc段中,由此可以知道该特性可以使得函数在编译时放到用户指定的段中。

 四、KEIL中实现自动初始化

        我们先给自动初始化一个定义:

                在不用显示的调用的情况下,可以由程序自动调用指定的初始化函数。

        那么知道了上面的知识点之后,能不能实现自动初始化的功能呢。我们先做一个尝试,

先看看程序编译后的段信息能否按照特定的规则按顺序排列。请看下面的代码:

 

__attribute__ ((section ("3"))) __attribute__((used)) void F1(void)
{
	
}

 __attribute__ ((section ("4"))) __attribute__((used)) void F2(void)
{
	
}

 
 __attribute__ ((section ("2"))) __attribute__((used)) void F3(void)
{
	
}

 __attribute__ ((section ("1"))) __attribute__((used)) void F4(void)
{
	
}


int main(void)
{
	while(1)
	{
		
	}
}

F1,F2,F3,F4分别设置在不同的段中,他们在文件中的顺序是随机的,下面我们编译该程序,看看map文件中是否有什么规律。

    F4                                       0x08000287   Thumb Code     2  rxm.o(1)
    F3                                       0x08000289   Thumb Code     2  rxm.o(2)
    F1                                       0x0800028b   Thumb Code     2  rxm.o(3)
    F2                                       0x0800028d   Thumb Code     2  rxm.o(4)

        在map文件中,我们找到如上的信息,可以看出编译器将我们的函数按照段名称的数值大小进行了排列

我们修改段名称在看看:

__attribute__ ((section ("b3"))) __attribute__((used)) void F1(void)
{

}

 __attribute__ ((section ("c4"))) __attribute__((used)) void F2(void)
{

}

 
 __attribute__ ((section ("a2"))) __attribute__((used)) void F3(void)
{

}

 __attribute__ ((section ("d1"))) __attribute__((used)) void F4(void)
{

}


int main(void)
{
	while(1)
	{
		
	}
}

结果如下:

    F3                                       0x08000287   Thumb Code     2  rxm.o(a2)
    F1                                       0x08000289   Thumb Code     2  rxm.o(b3)
    F2                                       0x0800028b   Thumb Code     2  rxm.o(c4)
    F4                                       0x0800028d   Thumb Code     2  rxm.o(d1)

        看样子,编译器是根据段的字符串名称进行排序的,这样的特性就可以使我们自动初始化的时候,可以轻易找到函数的地址了。

那么这样就可以通过第一个函数的地址,每次加2就可以计算出每个初始化函数的地址吗?

答案是不可以。

我们在函数中添加一些代码:

__attribute__ ((section ("b3"))) __attribute__((used)) void F1(void)
{
	int a=0;
	a++;
	
}

 __attribute__ ((section ("c4"))) __attribute__((used)) void F2(void)
{
	int a=0;
	a++;
	a++;
}

 
 __attribute__ ((section ("a2"))) __attribute__((used)) void F3(void)
{
	int a=0;
	a++;
	a++;
	a++;
}

 __attribute__ ((section ("d1"))) __attribute__((used)) void F4(void)
{
	int a=0;
	a++;
	a++;
	a++;
	a++;
}


int main(void)
{
	while(1)
	{
		
	}
}

编译后得到结果:

    F3                                       0x08000287   Thumb Code    10  rxm.o(a2)
    F1                                       0x08000291   Thumb Code     6  rxm.o(b3)
    F2                                       0x08000297   Thumb Code     8  rxm.o(c4)
    F4                                       0x0800029f   Thumb Code    12  rxm.o(d1)

这下好了,每个函数通过只加2不能准确计算出函数的地址了。这个方法行不通的啊。

那有什么办法能够固定的计算出函数的地址位置呢?

我们可以通过一个函数指针类型的变量记录函数的具体位置,函数指针的大小是固定的,那么这样不就可以精确的计算出函数的具体位置了吗?

下面我们再改造一下代码:

typedef void (*init_func)(void);//¶¨ÒåÒ»¸öº¯ÊýÖ¸ÕëÀàÐÍ
 
 
 
void F1(void)
{
	int a=0;
	a++;
	
}

__attribute__ ((section ("b3"))) __attribute__((used)) init_func init_func_f1 = F1;


void F2(void)
{
	int a=0;
	a++;
	a++;
}

__attribute__ ((section ("b3"))) __attribute__((used)) init_func init_func_f2 = F2;

 
void F3(void)
{
	int a=0;
	a++;
	a++;
	a++;
}

__attribute__ ((section ("b3"))) __attribute__((used)) init_func init_func_f3 = F3;

void F4(void)
{
	int a=0;
	a++;
	a++;
	a++;
	a++;
}

__attribute__ ((section ("b3"))) __attribute__((used)) init_func init_func_f4 = F4;


int main(void)
{
	while(1)
	{
		
	}
}

编译后的结果是:

    F1                                       0x080002a3   Thumb Code     6  rxm.o(i.F1)
    F2                                       0x080002a9   Thumb Code     8  rxm.o(i.F2)
    F3                                       0x080002b1   Thumb Code    10  rxm.o(i.F3)
    F4                                       0x080002bb   Thumb Code    12  rxm.o(i.F4)


    init_func_f1                             0x20000000   Data           4  rxm.o(b3)
    init_func_f2                             0x20000004   Data           4  rxm.o(b3)
    init_func_f3                             0x20000008   Data           4  rxm.o(b3)
    init_func_f4                             0x2000000c   Data           4  rxm.o(b3)

这里的init_func_f1 就是函数F1的函数指针,通过init_func_f1就可以找到F1函数的位置了,并且函数init_func_f1 到 init_func_f4 函数的位置的步长都是固定的。这样就好了。

那么最后我们如何调用这些初始化函数呢,总不能每次写一个函数,都给它指定一个段名称,还要自己取个名字吧,这样太麻烦了。

可以这样实现,通过两个已知的段的函数指针,把所有初始化函数指针都放在这两个函数指针中间,就可以调用这些函数了。

我们改造一下代码:

 
typedef void (*init_func)(void);
 
#define DRV_INIT_S(func) __attribute__ ((section ("DRV.1"))) __attribute__((used)) init_func init_func_##func = func
#define DRV_INIT_E(func) __attribute__ ((section ("DRV.3"))) __attribute__((used))init_func init_func_##func  = func
#define DRV_INIT(func)   __attribute__ ((section ("DRV.2"))) __attribute__((used)) init_func init_func_##func = func
 
 
void DRV_INIT_S_FUNC(void){}
DRV_INIT_S(DRV_INIT_S_FUNC);
 
void DRV_INIT_E_FUNC(void){}
DRV_INIT_E(DRV_INIT_E_FUNC);
	
 
void F3(void)
{
	int a=0;
	a++;
	
}

DRV_INIT(F3);
	
void F2(void)
{
	int a=0;
	a++;
	
}

DRV_INIT(F2);
	
	
void F1(void)
{
	int a=0;
	a++;
	
}

DRV_INIT(F1);


int main(void)
{
	while(1)
	{
		
	}
}



程序中DRV_INIT_S用于指定函数在DRV.1段中,DRV_INIT_E用于函数在DRV.3段中,DRV_INIT用于指定函数在DRV.2段中,由于编译器的排序,所以通过DRV_INIT注册的函数都会在DRV.1到DRV.2段之间,这样的换,通过遍历这两个段中间的函数指针,即可调用所有初始化函数,下面是map文件结果:

    init_func_DRV_INIT_S_FUNC                0x20000000   Data           4  rxm.o(DRV.1)
    init_func_F3                             0x20000004   Data           4  rxm.o(DRV.2)
    init_func_F2                             0x20000008   Data           4  rxm.o(DRV.2)
    init_func_F1                             0x2000000c   Data           4  rxm.o(DRV.2)
    init_func_DRV_INIT_E_FUNC                0x20000010   Data           4  rxm.o(DRV.3)

从结果来看,函数指针的排序顺序和在代码文件中的顺序相同,这样初始化顺序也可以控制了。

那么如何调用这些初始化函数呢,再改造一下代码:

 
typedef void (*init_func)(void);
 
#define DRV_INIT_S(func) __attribute__ ((section ("DRV.1"))) __attribute__((used)) init_func init_func_##func = func
#define DRV_INIT_E(func) __attribute__ ((section ("DRV.3"))) __attribute__((used))init_func init_func_##func  = func
#define DRV_INIT(func)   __attribute__ ((section ("DRV.2"))) __attribute__((used)) init_func init_func_##func = func
 
 
void DRV_INIT_S_FUNC(void){}
DRV_INIT_S(DRV_INIT_S_FUNC);
 
void DRV_INIT_E_FUNC(void){}
DRV_INIT_E(DRV_INIT_E_FUNC);
	
 
void F3(void)
{
	int a=0;
	a++;
	
}

DRV_INIT(F3);
	
void F2(void)
{
	int a=0;
	a++;
	
}

DRV_INIT(F2);
	
	
void F1(void)
{
	int a=0;
	a++;
	
}

DRV_INIT(F1);


void DO_DRV_INIT(void)
{
	init_func* p=0;
	for(p=&init_func_DRV_INIT_S_FUNC;p<=&init_func_DRV_INIT_E_FUNC;p++)
	{
		(*p)();
	}
}

int main(void)
{
	DO_DRV_INIT();
	while(1)
	{
		
	}
}

由于宏定义已经为我们定义了函数指针init_func_DRV_INIT_S_FUNC 和 init_func_DRV_INIT_E_FUNC ,通过这两个函数指针的地址,就可以计算出所有初始化函数的地址,再调用这些函数即可实现自动初始化函数的调用。

最后,很多人可能会问这样做之后,函数的调用顺序,会不会乱掉,我们再分析的时候,会不会找不到函数的初始化顺序?

其实不用担心,看看下面的工程:

其中,dev_test2.c和dev_test1.c是模拟的驱动文件,drv.h是自动初始化框架的公共头文件。

代码如下:

 dev_test2.c

#include "drv.h"

void F4()
{
	
}

DRV_INIT(F4);

void F3()
{
	
}

DRV_INIT(F3);

dev_test1.c

#include "drv.h"

void F2()
{
	
}

DRV_INIT(F2);

void F1()
{
	
}

DRV_INIT(F1);

编译后结果如下:

    init_func_DRV_INIT_S_FUNC                0x20000000   Data           4  rxm.o(DRV.1)
    init_func_F4                             0x20000004   Data           4  dev_test2.o(DRV.2)
    init_func_F3                             0x20000008   Data           4  dev_test2.o(DRV.2)
    init_func_F2                             0x2000000c   Data           4  dev_test1.o(DRV.2)
    init_func_F1                             0x20000010   Data           4  dev_test1.o(DRV.2)
    init_func_DRV_INIT_E_FUNC                0x20000014   Data           4  rxm.o(DRV.3)

可以看出,初始化函数的顺序是先按照文件的排序,再按照文件中代码的顺序排序的。

那么我们只要调整一下文件的顺序:

那它的顺序就是:

    init_func_DRV_INIT_S_FUNC                0x20000000   Data           4  rxm.o(DRV.1)
    init_func_F2                             0x20000004   Data           4  dev_test1.o(DRV.2)
    init_func_F1                             0x20000008   Data           4  dev_test1.o(DRV.2)
    init_func_F4                             0x2000000c   Data           4  dev_test2.o(DRV.2)
    init_func_F3                             0x20000010   Data           4  dev_test2.o(DRV.2)
    init_func_DRV_INIT_E_FUNC                0x20000014   Data           4  rxm.o(DRV.3)

这样的话,其实我们的自动初始化函数的初始化顺序,一眼就能看出来了,还是非常方便的。

经过实验之后,可以得出结论,KEIL中自动初始化的顺序具体如下:

1、先按分组排序

2、再按文件排序

3、再按代码排序

五、总结

        通过研究之后,我们可以使用编译器给出的特性,实现自动初始化的功能。

它的优点如下:

1、可以帮我们省去驱动头文件管理的麻烦。

2、可以帮助我们实现程序的模块化关联,比如上图中删除dev_test2.c,其实对整个工程没有影响.不需要修改main函数代码。

3、初始化顺序结构清晰,很容易实现初始化顺序的修改。

4、使程序结果清晰明了,并强制使我们的代码结构规范,易懂易维护。

它的缺点是:

1、程序结构复杂化了。

2、使用它需要一定学习成本。

3、需要更改代码书写习惯。

附上我测试使用的工程:单片机C语言骚 *** 作__模块化思想__1.自动初始化-Linux文档类资源-CSDN文库

这个特性是模块化的一部分,后面还会写别的,请关注

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/734106.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-27
下一篇 2022-04-27

发表评论

登录后才能评论

评论列表(0条)

保存