大家在编写单片机程序的时候,不知道有没有一种感觉,就是在写驱动程序的时候,很多时候,每一个驱动的初始化函数,都需要通过头文件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文库
这个特性是模块化的一部分,后面还会写别的,请关注
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)