如何实现 MCU软件中多个模块初始化函数的优雅调用

  目录

序言

做嵌入式开发的需要把需要把板载的每一个外围设备都进行初始化,有时候我们为了代码的可读性、易移植性会把每个模块单独封装,使用不同的.c和.h文件进行管理,在模块对应的.h文件中会把向外开放的API接口申明出来,方便上层调用,其中模块的初始化代码就包含在开放的API中,这样就使得上层的软件需要依赖于底层的接口,当底层变化时或头文件依赖更改时,需要重新修改代码,模块少还好点,模块多的时候就非常痛苦了。同时也带来另一个问题,软件中每增加一个模块都要在主函数中增加模块的初始化代码,每删减一个模块,都要在主函数的初始化代码中删减一个模块,这样无疑是不友好的。
那Linux中我新写的模块为什么不需要这样操作呢?单片机的开发中能不能也同样实现这样的功能呢?
答案肯定是可以的,怎么做下面我们一起来看下。

一般的模块化开发

假如我现在开发的系统中有3个模块,分别是GPS、陀螺仪、触摸屏,正常我们在软件设计中会分别建立以下文件用于初始化这些模块的硬件接口以及驱动代码如下:

  • 硬件接口层:
    实现硬件接口的初始化,如串口、I2C、SPI等,配置GPIO模式以及中断优先级等。
  • 驱动层:
    实现外围模块内部寄存器的配置或初始化模块的工作模式等。
  • 应用层:
    业务逻辑处理。

按照这个逻辑就是下层向上层提供一个接口用于初始化,最终在APP程序中调用初始化程序。而主函数中可能是这样的代码结构:

#include "main.h"
#include "drv_gps.h"
#include "drv_lcd.h"
#include "drv_mpu6050.h"

int main(void)
{
  /* GPS模块初始化 */
  drv_gps_init();
  /* LCD初始化 */
  drv_lcd_init();
  /* 陀螺仪初始化 */
  drv_mpu6050_init();

  for(;;)
  {
    /* ... */
  }
}

上面的代码中每增加一个模块都需要包含对应的头文件,在main函数中也要修改初始化的代码,去掉一个模块也是这样的,增加了软件的耦合度,无疑不是明智的,下面就来探索下怎么自动的实现模块的初始化,在不修改main函数的代码的前提下,同时优雅的初始化模块。

实现思路

把每个模块的初始化代码用C拓展语法中的__attribute__设置其属性,定义在某一个段(section)中,这样我们只要在初始化的时候把这个段中所有的函数执行一遍就可以了。
新问题来了,为什么放在某一段中就能运行呢?
我们知道,函数名本身就是一个地址,用指针指向这个地址时,调用这个指针去运行这段代码就可以了,而我们把函数放在这个段中,我们是可以获取段的起始和终止地址的,有了这些条件,就能把段中的每个函数都遍历运行一遍了。

关键代码

有了上述思路后,我们先来看下我的main函数的代码实现。

#include "main.h"

int main(void)
{  
  /* System module initialization */
  System_Config();
}
    不管新增或删减什么模块都不要改main函数,关键就在于System_Config()函数的实现,下面就来分析下:
/*
* @ brief : Initialization of each module.
* @ param : None.
* @ return: None.
* @ author: bagy
* @ modify: None.
*/
void System_Config(void)
{
  volatile const init_func *index;

  for(index = &_init_System_InitSectionStart_func; index < &_init_System_InitSectionEnd_func; index++)
  {
    (*index)();
  }
}

怎么样,看到这个代码是不是人傻了,啥玩意这是?
假如我离职,交接给下一任工程师,我相信他也是崩溃的,马上提桶的心都有了,其实,对于懂的人来说并不难,所以为了你交接工作时看到这样的代码不慌,还要耐心看完这篇文章啊。
结合上面分析的代码思路可以更好的理解上面的代码,本质就是把段表里的函数全部遍历、运行。
再深入分析上述代码,看下init_func是怎么定义的。

typedef  void (*init_func)(void);

就是定义了一个函数指针,这里要注意,模块的初始化代码要遵循这个函数指针的定义,这个因人而异,想怎么修改都是没问题的。

下面再看下怎么获取段表的起始和结束的地址的,这里定义两个函数,把这两个函数分别导出到段表的起始和结束地址,通过这两个函数就能获取起止地址了。

/*
* @ brief : Initialize the start function of the segment, used to mark the start address.
* @ param : None.
* @ return: None.
* @ author: bagy
* @ modify: None.
*/
static void System_InitSectionStart(void)
{
  return;
}
INIT_EXPORT(System_InitSectionStart, "0");

/*
* @ brief : Initialize the end function of the segment to mark the end address.
* @ param : None.
* @ return: None.
* @ author: bagy
* @ modify: None.
*/
static void System_InitSectionEnd(void)
{
  return;
}
INIT_EXPORT(System_InitSectionEnd, "z");

函数很简单,但是INIT_EXPORT有是啥东西,里面的两个参数又是啥意思?

这里INIT_EXPORT只是一个宏定义,来看下这个宏的真面目吧

#define  INIT_SECTION(x)      __attribute__((section(x))) 
#define  INIT_USED            __attribute__((used)) 
#define  INIT_EXPORT(fn, level)    INIT_USED init_func _init_##fn##_func INIT_SECTION(".init_fn."level) = fn
  • fn
    表示到导出的函数。
  • level
    表示等级,数值越小Keil就会把对应的函数放在段表的靠前的位置。因此上面获取起始段表的地址哪里后面的等级是0,也就是段表中的第一个函数。

思维发散一下,还可以通过这个level参数,控制函数先后调用的顺序。这点是很有必要的,在设计过程中可能会涉及模块的依赖问题,比如A模块的初始化成功要依赖于B模块,因此要先运行B模块的初始化函数,使用这个参数就能很好的解决这个问题了。

这样在每个模块中就只要用INIT_EXPORT这个宏导出即可,增加或删减一个模块的时候是不是初始化的代码都不用修改。这种体验就是:爽!

map文件分析

接手了这样的代码后怎么更快的确认函数的调用关系呢?.map文件来帮忙。下面来查看下我的项目中的map文件。

这样函数的先后调用关系就非常清楚了。
到此优雅、简单的调用就实现了,目的就是增删模块尽可能的少修改代码,果然懒才是人类进步的阶梯啊!


评论