有限状态机FSM(Finite State Machine)自动初始化
1、自动初始化简介
自动初始化机制是指初始化函数不需要被显式调用,只需要在函数定义处通过宏定义的方式进行申明,就会在系统启动过程中被执行。
例如我们在裸机上初始化各个外设时,一般把初始化函数放到main函数中,那么造成代码篇幅过长,为了解决这个问题,RT_thread和OneOS也是用了自动初始化机制,这样有利于代码简洁和整体上比较模块化,在传统裸机上,初始化是使用如下几个方式来初始化函数:
第一种:全部放置在main函数里面定义,如:
int main(void)
{HAL_Init(); /* 初始化HAL库 */sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */delay_init(72); /* 延时初始化 */usart_init(115200); /* 串口初始化为115200 */usmart_dev.init(72); /* 初始化USMART */led_init(); /* 初始化LED */lcd_init(); /* 初始化LCD */……while(1){;}
}
第二种:将所有初始化函数放到某一个函数中,如_init(),然后在main中调用这个函数;
上述两种初始化方式都会导致初始化代码篇幅过长,参考Rt-thread和OneOS的自动初始化,可以用于初始化代码规范编写。
2、RT-Thread初始化设计
2.1、自动初始化优点
RT-Thread的自动初始化机制通过INIT_EXPORT宏实现,将初始化函数放入特定段,系统启动时自动按顺序执行。该机制分为多个优先级,确保依赖关系正确。它有以下技术优势:
- 模块化设计
各组件通过宏声明初始化依赖,无需手动调用初始化函数。
- 优先级控制
通过段名排序实现严格的初始化顺序控制。
- 可扩展性
新增组件只需添加INIT_EXPORT声明,无需修改主初始化逻辑。
这种机制有效解决了嵌入式系统中组件初始化顺序的管理问题,是RT-Thread实现高度模块化设计的关键技术之一。
2.2、初始化段分类
如下表所示。
2.3、初始化执行流程
void rt_components_board_init(void)
和void rt_components_init(void)
函数是专门用来负责自动初始化的。
void rt_components_board_init(void)
{
#if RT_DEBUG_INITint result;const struct rt_init_desc *desc;for (desc = &__rt_init_desc_rti_board_start; desc < &__rt_init_desc_rti_board_end; desc ++){rt_kprintf("initialize %s", desc->fn_name);result = desc->fn();rt_kprintf(":%d done\n", result);}
#elsevolatile const init_fn_t *fn_ptr;for (fn_ptr = &__rt_init_rti_board_start; fn_ptr < &__rt_init_rti_board_end; fn_ptr++){(*fn_ptr)();}
#endif /* RT_DEBUG_INIT */
}/*** @brief RT-Thread Components Initialization.*/
void rt_components_init(void)
{
#if RT_DEBUG_INITint result;const struct rt_init_desc *desc;rt_kprintf("do components initialization.\n");for (desc = &__rt_init_desc_rti_board_end; desc < &__rt_init_desc_rti_end; desc ++){rt_kprintf("initialize %s", desc->fn_name);result = desc->fn();rt_kprintf(":%d done\n", result);}
#elsevolatile const init_fn_t *fn_ptr;for (fn_ptr = &__rt_init_rti_board_end; fn_ptr < &__rt_init_rti_end; fn_ptr ++){(*fn_ptr)();}
#endif /* RT_DEBUG_INIT */
}
2.4、启动顺序
- 系统启动后调用rtthread_startup()
- 调用RT_WEAK void rt_hw_board_init(void)
- 执行rt_components_board_init()遍历.rti_fn段
- 按段名排序顺序调用各初始化函数
- 启动main线程
- 执行void rt_components_init(void)遍历.rti_fn段
- 按段名排序顺序调用各初始化函数
rt_components_board_init()函数最先执行,这个函数是用来初始化芯片相关的硬件的,这个函数会遍历用 INIT_BOARD_EXPORT(fn)声明的函数列表。
rt_components_init()函数是在系统启动后,在main线程里面被调用执行,这个函数是用来初始化其他用 INIT_XXX_EXPORT(fn)声明的函数列表的。
2.5、典型初始化函数示例
int uart1_init()
{ struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT; /* 初始化配置参数 */rt_err_t res;uart1Serial = rt_device_find(UART1_NAME);if (!uart1Serial){LOG_E("find %s failed!", UART1_NAME);return RT_ERROR;}config.baud_rate = BAUD_RATE_4500000; //修改波特率为 100000config.data_bits = DATA_BITS_8; //数据位 9config.stop_bits = STOP_BITS_1; //停止位 1config.bufsz = 1024; //修改缓冲区 buff size 为 128config.parity = PARITY_NONE; //偶校验位res = rt_device_control(uart1Serial, RT_DEVICE_CTRL_CONFIG, &config); if(res != RT_EOK){LOG_E("commit control err!");return RT_ERROR;}return RT_EOK;
}INIT_PREV_EXPORT(uart1_init);
程序中调用INIT_PREV_EXPORT(uart1_init);查看map文件如下:
程序启动会按顺序加载0x08072648这个地址从而执行uart1_init这个函数。
3、自动初始化原理
自动初始化原理也是挺简单理解的,简单来说,就是遍历初始化函数的地址,然后执行到该地址时就会调用该地址的初始化函数,首先我们定义一个地址区域,该地址区域就是存放我们的初始化函数地址的区域,如以下源码所示:
static int32_t lv_device_init_start(void)
{return 0;
}LV_INIT_EXPORT(lv_device_init_start, "1.", "");static int lv_device_init_end(void)
{return 0;
}LV_INIT_EXPORT(lv_device_init_end, "1.end", "");
上述源码表示:定义两个地址,分别为外设初始化开始地址,外设初始化结束地址,首先关注重要的函数是以下两个函数:
LV_INIT_EXPORT(lv_device_init_start, "1.", "");
LV_INIT_EXPORT(lv_device_init_end, "1.end", "");
上述的函数可以在某个头文件定义,如以下源码所示:
typedef signed int lv_int32_t;typedef lv_int32_t lv_err_t;typedef lv_err_t (*lv_init_fn_t)(void);#define LV_INIT_PRIO_HIGH "1"
#define LV_INIT_PRIO_MIDDLE "2"
#define LV_INIT_PRIO_LOW "3"#define LV_SECTION(x) __attribute__((section(x)))
#define LV_USED __attribute__((used))#define LV_INIT_EXPORT(lv_fn, lv_level, lv_sublevel) \
LV_USED const lv_init_fn_t _lv_call_##lv_fn \
LV_SECTION(".init_call." lv_level lv_sublevel) = lv_fn
下面我们来分析自动初始化机制的原理,首先我们注意一个代码段,如以下所示:
#define LV_SECTION(x) __attribute__((section(x)))
__attribute__((section(x)))的代码段定义的是x存储函数地址或者变量地址,然后
LV_SECTION(".init_call." lv_level lv_sublevel) = lv_fn
代码段表示“.init_call.1.” = lv_fn,注意:x = “.init_call.1.”,所以“.init_call.1.”字符串指向fn(fn就是函数地址)。
“##”在c语言中必表示字符串连接符,所以我们得出以下代码:
LV_USED const lv_init_fn_t _lv_call_lv_device_int_start ".int_call.1."
同理,LV_INIT_EXPORT(lv_device_init_end, ".1.end", "");也是一样的操作,所以得到以下代码:
LV_USED const lv_init_fn_t _lv_call_lv_device_init_end ".init_call.1.end"
自动初始化的根本原理就是把函数加入符号段,其实就是使用了MDK编译器的__attribute__((section(x)))关键字,对函数进行声明,通过section关键字进行声明的函数,在编译器进行链接的时候,就会自动收集这些函数并把他们放到一个集中的区域里面,查看以下.map文件可知。
4、自动初始化示例
在裸机系统中,我们怎么制作自动初始化机制呢,首先我们在外设初始化函数中略加修改,例如led_init()函数,如以下源码所示:
void led_init(void)
{
/* 定义引脚的函数…此处省略 */
}
如果使用自动初始化,那么led_init()函数需要修改,如以下源码所示:
static int led_init(void)
{/* 定义引脚的函数…此处省略 */return 0;
}
LV_DEVICE_INIT(led_init, LV_INIT_PRIO_LOW);
注意:必须return0,下面调用LV_DEVICE_INIT()外设初始化,其他外设也是一样的道理。
遍历初始化函数,如以下源码所示:
static lv_err_t _k_device_auto_init(void)
{const lv_init_fn_t *lv_fn_ptr_init_start; /* 开始地址 */const lv_init_fn_t *lv_fn_ptr_init_end; /* 结束地址 */const lv_init_fn_t *lv_fn_ptr; /* 当前地址 */lv_err_t ret;/* 获取初始化开始地址 */
lv_fn_ptr_init_start = &_lv_call_lv_device_init_start + 1;/* 获取初始化结束地址 */lv_fn_ptr_init_end = &_lv_call_lv_device_init_end - 1;for (lv_fn_ptr = lv_fn_ptr_init_start;
lv_fn_ptr <= lv_fn_ptr_init_end;
lv_fn_ptr++)
/* 遍历地址 */{ret = (*lv_fn_ptr)();/* 执行地址的函数 */if (ret != 0){return ret;}}return 0;
}
上述源码可知:初始化开始地址不正是LV_INIT_EXPORT(lv_device_init_start,"1.", "");函数吗,而初始化结束地址不正是LV_INIT_EXPORT(lv_device_init_end,"1.end", "");,然后我们把led_init()函数地址插入这地址区域中,如下图所示:
最后在main函数这样定义即可,如以下源码所示:
int main(void)
{lv_err_t ret;/* 外设初始化 */ret = _k_device_auto_init();if (0 != ret){printf("\r\n device startup failed\r\n");}................while(1);
}
同理,应用层的初始化也可以放在_k_device_auto_init中一同处理。
参考链接:技术干货!裸机系统自动初始化详解 - 哔哩哔哩
rt-thread嵌入式操作系统自动初始化详解-CSDN博客
https://zhuanlan.zhihu.com/p/636316166