Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
RSOC-2026
【RSOC26】rtthread启动流程
发布于 2026-08-18 18:28:36 浏览:18
订阅该版
[tocm] ```markdown ## RT-Thread ## RT-Thread 启动流程 课堂笔记。对应源码:`src/components.c`(`rtthread_startup`)、`src/timer.c`(`rt_system_timer_thread_init` / `_timer_thread_entry`)、`src/idle.c`。 --- ### 一、启动总览  上电复位之后,不是直接进你写的 `main()`,而是: ```c Reset_Handler(汇编) → SystemInit() // 时钟、FPU 等芯片相关 → $Sub$$main / entry // 编译器把原 main 劫持到 RT-Thread 入口 → rtthread_startup() → rt_hw_interrupt_disable() // 关中断,一直关到调度器 start → rt_hw_board_init() // 板级:时钟、堆、串口、SysTick(会启动 tick 硬件) → rt_show_version() → rt_system_timer_init() // 硬定时器链表 → rt_system_scheduler_init() // 优先级队列、位图 → rt_application_init() // 创建 main 线程 → rt_system_timer_thread_init() // 软件定时器线程(开了才有) → rt_thread_idle_init() // 空闲线程 → rt_system_scheduler_start() // 切第一个线程,这里才开中断;函数不再返回 ``` 调度器一旦启动,CPU 交给优先级最高的就绪线程。之后看到的 `main`、`timer`、`tidle` 都是**普通线程**,只是内核自己创建的。 #### 为什么一进 `rtthread_startup()` 先关中断 不是「调用 `rtthread_startup` 之前」关的,而是这个函数**第一句** `rt_hw_interrupt_disable()`。Cortex-M 复位后 PRIMASK=0,CPU 层面中断其实是开着的,只是 SysTick、UART 这些源大多还没使能,所以从 `Reset_Handler` 走到这里,表面上很安静。 真正危险的是下一句 `rt_hw_board_init()`:板级初始化会配时钟、开堆、开串口,并且**把 SysTick 跑起来**(LOAD / TICKINT / ENABLE)。如果这时全局中断还开着,SysTick 马上就能打进 ISR。而此刻: - 定时器链表还没 `rt_system_timer_init()` - 就绪队列 / 优先级位图还没 `rt_system_scheduler_init()` - `main` / `timer` / `idle` 线程都还不存在 - `rt_current_thread` 还是空的 SysTick ISR 却已经会做 `rt_tick++`、`rt_timer_check()` 扫链表、必要时 `rt_schedule()`。链表是空的、当前线程是空的,结果就是野指针 / HardFault,看起来像随机复位。 所以关中断是把两件事错开:**硬件可以开始报时,但 ISR 暂时进不来**,直到内核能接住这记报时。 ``` 关中断 → board_init:SysTick 硬件开始数,ISR 被 PRIMASK 挡住 → 定时器链表、调度器、三个系统线程全部建好 → rt_thread_startup() 往就绪队列挂线程(中间绝不能被 tick 切走半截) → rt_system_scheduler_start() → 选出优先级最高的就绪线程(一般是 main) → rt_hw_context_switch_to() 切过去 → 这里才 cpsie i / 开 PRIMASK 此后 tick 打进来就是合法的:链表在、线程在、调度器在。 ``` 几条容易混的: 1. **不是关到 board_init 结束就开。** 一直关到调度器 start。中间创建线程、插入就绪队列,都属于「数据结构半成品」,不能被中断走进去。 2. **SysTick 使能 ≠ 中断已经在跑。** 计数器硬件可以在关中断期间走;只是 TICKINT 被 PRIMASK 挡住,`rt_tick` 这段时间不加。少掉的那几毫秒启动阶段可以忽略。 3. **`scheduler_start` 不会返回**,所以开头的 disable 不必在 `rtthread_startup` 里配对 enable。开中断是第一次线程切换的副作用。 4. **`rt_hw_interrupt_disable()` 还会把内核的中断锁层级放到已知状态。** 后面成对的 disable/enable 不会因为复位后 PRIMASK 不确定,中途把中断提前打开。 --- ### 二、汇编窗口进内核 汇编窗口进入:  启动函数入口:  Keil 里从复位单步,典型路径: 1. `Reset_Handler`:设 MSP、调 `SystemInit`、跳到 C 入口。 2. ARMCC 用 `$Sub$$main` 包一层:先跑 RT-Thread 启动,再把用户 `main` 塞进 main 线程。 3. GCC 工程常见入口是 `entry()`,内部同样调 `rtthread_startup()`。 启动流程(`rtthread_startup` 调用链):  记住两件和“谁在跑”有关的事: - **SysTick 在 `rt_hw_board_init()` 里配好**。它只负责报时:`rt_tick++`,并检查定时器链表。它不是一个线程。 - **用户 `main()` 此时还没跑。** `rt_application_init()` 只是把 main 线程 `init` + `startup` 进就绪队列;真正执行要等到 `rt_system_scheduler_start()`。 --- ### 三、调度起来之后的三个系统线程 | 线程名 | 怎么来的 | 干什么 | | ------------------ | ------------------------------- | --------------------------------------------------- | | `main` | `rt_application_init()` | 跑用户 `main()`,应用入口 | | `timer` | `rt_system_timer_thread_init()` | **软件定时器回调**(开 `RT_USING_TIMER_SOFT` 才有) | | `tidle` / `tidle0` | `rt_thread_idle_init()` | 空闲钩子、对象清理、可选低功耗;优先级最低 | #### 1. main 函数线程  - 静态创建,栈大小 `RT_MAIN_THREAD_STACK_SIZE`,优先级 `RT_MAIN_THREAD_PRIORITY`(一般偏低,把 CPU 让给业务线程)。  - 入口里调用户 `main()`。`main` 里 `return` 之后这个线程就结束,系统继续跑其它线程 + idle。 - 所以:你在 `main()` 里看到的第一行代码,已经是**线程上下文**,调度器、SysTick、idle 都已经在了。 #### 2. 定时器线程(软件定时器)  `timer` 线程处理的是**软件定时器**,不是第二套时钟。时间基准仍然是 **SysTick 累加的 `rt_tick`**。软/硬的差别不在“用不用 SysTick”,而在**到期后回调跑在哪**。 #### 3. 空闲线程  - 优先级 = `RT_THREAD_PRIORITY_MAX - 1`(数字最大,最低)。 - 就绪队列空了才跑:执行 `rt_thread_idle_sethook()` 挂的钩子、回收 `RT_THREAD_CLOSE` 的动态线程、有的 BSP 在这里进 WFI。 - 不要在 idle 钩子里做长时间阻塞;它一堵,系统就再也进不了“没事可做”的状态。 --- ### 四、同一只挂钟,两种响法 SysTick ISR 每次只做“报时”: 1. `rt_tick++`(系统时间往前走一格) 2. 扫定时器链表: - **硬定时器**(`RT_TIMER_FLAG_HARD`)到期 → **在 ISR 里直接调回调** - **软定时器**(`RT_TIMER_FLAG_SOFT`)到期 → **唤醒名为 `"timer"` 的线程**,回调放到线程里再跑 软定时器没有自己的晶振,也没有第二套 tick。`rt_timer_start()` 里写的 timeout,单位就是系统 tick;`_timer_thread_entry` 里的 `rt_tick_get()`、`rt_thread_delay(next_timeout)`,数的也是同一根尺子。 | | 硬定时器 `RT_TIMER_FLAG_HARD` | 软定时器 `RT_TIMER_FLAG_SOFT` | | ------------ | ---------------------------------------- | ------------------------------------------------------------ | | 时间从哪来 | 都是 `rt_tick`(通常由 SysTick 加) | 同上 | | 谁发现到期 | SysTick ISR | ISR 只负责“到点了”,真正干活的是 `timer` 线程 | | 回调跑在哪 | 中断上下文 | 线程上下文(`_timer_thread_entry` → `rt_soft_timer_check()`) | | 回调能干什么 | 必须短、不能阻塞、不能随便调会挂起的 IPC | 可以稍重:发信号量、申请内存、调阻塞 API(仍不建议特别慢) | 为什么拆开:SysTick 常见 1ms 一次。ISR 里跑重活会拖长关调度/关中断的时间,别的线程和中断都跟着抖。软定时器把“闹钟到了”和“响了之后干什么”拆开——**报时仍在中断,干活在 `timer` 线程**。 没用软定时器、关了 `RT_USING_TIMER_SOFT` 时,这个线程不会建;硬定时器照样走 SysTick,回调全部挤在中断里。 --- ### 五、`rt_system_timer_thread_init()` 源码 ```c void rt_system_timer_thread_init(void) { #ifdef RT_USING_TIMER_SOFT int i; for (i = 0; i < sizeof(_soft_timer_list) / sizeof(_soft_timer_list[0]); i++) { rt_list_init(_soft_timer_list + i); } /* start software timer thread */ rt_thread_init(&_timer_thread, "timer", _timer_thread_entry, RT_NULL, &_timer_thread_stack[0], sizeof(_timer_thread_stack), RT_TIMER_THREAD_PRIO, 10); /* startup */ rt_thread_startup(&_timer_thread); #endif /* RT_USING_TIMER_SOFT */ } ``` 只在开了 `RT_USING_TIMER_SOFT` 时执行,三步: 1. 初始化 `_soft_timer_list`(按超时时间排序的软定时器链表,跳表/多级列表,加快“下一个到期”查询)。 2. **静态**创建名为 `"timer"` 的线程:控制块 `_timer_thread`、栈 `_timer_thread_stack` 都在 BSS 里,不用堆。入口 `_timer_thread_entry`,优先级 `RT_TIMER_THREAD_PRIO`(一般比普通应用线程高,保证回调及时),时间片 10 tick。 3. `rt_thread_startup` 把它挂到就绪队列;调度器启动后就会跑。 这就是静态线程那一套:`rt_thread_init` + `rt_thread_startup`,和自己写 `thread1` 的方式相同,只是对象是内核全局变量。 --- ### 六、`_timer_thread_entry()`:闹钟服务员 ```c static void _timer_thread_entry(void *parameter) { rt_tick_t next_timeout; while (1) { /* get the next timeout tick */ if (_timer_list_next_timeout(_soft_timer_list, &next_timeout) != RT_EOK) { /* no software timer exist, suspend self. */ rt_thread_suspend(rt_thread_self()); rt_schedule(); } else { rt_tick_t current_tick; current_tick = rt_tick_get(); if ((next_timeout - current_tick) < RT_TICK_MAX / 2) { next_timeout = next_timeout - current_tick; rt_thread_delay(next_timeout); } } /* check software timer */ rt_soft_timer_check(); } } ``` 永远循环,逻辑可以压成三句话: ``` 查下一个软定时器何时到期 没有定时器 → 自己挂起,让出 CPU 有、且还没到点 → rt_thread_delay(剩余 tick) 到点 / delay 被提前唤醒 → rt_soft_timer_check() 扫链表、调回调 ``` #### 细节 1. **`_timer_list_next_timeout()` 失败** 当前一个软定时器都没有。线程 `rt_thread_suspend` 自己,再 `rt_schedule()` 切走。之后有人 `rt_timer_start()` 一个软定时器,内核会把这个线程唤醒,循环从头再查。 2. **`(next_timeout - current_tick) < RT_TICK_MAX / 2`** `rt_tick_t` 无符号,直接减会在溢出时绕回。这个不等式是内核判断“还没到期”的惯用写法(距离小于半圈就认为是未来)。还没到就 `rt_thread_delay` 睡到那个 tick。 3. **`rt_thread_delay(next_timeout)` 不是另起一套定时** 只是让 `timer` 线程睡 N 个 tick;这 N 个 tick 还是 SysTick 在减。中间如果有人新 `rt_timer_start` 了一个**更早到期**的软定时器,内核会提前把这个线程叫醒,再重新算下一个超时。 4. **`rt_soft_timer_check()`** 在**线程上下文**取出所有已到期的软定时器,执行回调。这里可以调 IPC、申请内存——这是和硬定时器最大的区别。周期性软定时器会在这里被重新挂回链表。 --- ### 七、和启动顺序对上号(容易混的点) 1. `rt_system_timer_init()` ≠ `rt_system_timer_thread_init()` - 前者:硬/软定时器**链表**都建好,给 SysTick ISR 用。 - 后者:只建 `"timer"` **线程**,专门跑软定时器回调。 2. SysTick 中断比 `timer` 线程更早就在工作。 板级初始化里已经 `rt_hw_systick` 了;只是调度器没 start 之前,即便软定时器到期也还没有线程可唤醒。所以内核把 timer 线程的创建放在 `rt_application_init()` 之后、`scheduler_start` 之前。 3. 调试时 `list_thread` 看到 `timer` 长期 `suspend` 是正常的:没有软定时器时它就睡。一 `rt_timer_start(SOFT)` 它会被叫醒。 4. 自己写超时优先用软定时器;只有“必须在 1ms 中断里立刻拨脚/置标志”才用硬定时器,回调里不要 `printf`、不要调会阻塞的 API。 --- ### 八、`INIT_BOARD_EXPORT` 与 `INIT_APP_EXPORT` 这两个宏不是“函数调用”,而是**把初始化函数指针塞进指定的链接段**,启动时内核按段顺序扫一遍、挨个调用。差别只在**哪一段、什么时候扫**。 | | `INIT_BOARD_EXPORT(fn)` | `INIT_APP_EXPORT(fn)` | | ------------ | --------------------------------------------------- | ---------------------------------------- | | 展开成 | `INIT_EXPORT(fn, "1")` | `INIT_EXPORT(fn, "6")` | | 函数指针放哪 | `.rti_fn.1` | `.rti_fn.6` | | 谁来扫 | `rt_hw_board_init()` → `rt_components_board_init()` | main 线程入口 → `rt_components_init()` | | 调度器 | **还没 start**,关着中断 | **已经在跑**,你在 `main` 线程里 | | 能干什么 | 挂设备、配引脚、注册串口 | 建线程、用 IPC、`rt_thread_delay` | | 不能干什么 | 创建线程、延时、等信号量(会踩空) | 依赖“板级还没就绪”的硬件(应放到 BOARD) | `fn` 原型必须是:`int fn(void);`,返回 `0` 表示成功。自动 init 要开 `RT_USING_COMPONENTS_INIT`(Studio 默认开)。Nano 内核常常没有这套,就得自己在 `main` 里调。 #### 1. 宏怎么展开 `rtdef.h` 里(开了 `RT_USING_COMPONENTS_INIT`): ```c typedef int (*init_fn_t)(void); #define INIT_EXPORT(fn, level) \ rt_used const init_fn_t __rt_init_##fn rt_section(".rti_fn." level) = fn #define INIT_BOARD_EXPORT(fn) INIT_EXPORT(fn, "1") #define INIT_PREV_EXPORT(fn) INIT_EXPORT(fn, "2") #define INIT_DEVICE_EXPORT(fn) INIT_EXPORT(fn, "3") #define INIT_COMPONENT_EXPORT(fn) INIT_EXPORT(fn, "4") #define INIT_ENV_EXPORT(fn) INIT_EXPORT(fn, "5") #define INIT_APP_EXPORT(fn) INIT_EXPORT(fn, "6") ``` `rt_section` / `rt_used` 就是编译器属性: ```c #define rt_section(x) __attribute__((section(x))) #define rt_used __attribute__((used)) /* 防止没被引用被链接器丢掉 */ ``` 所以你写: ```c int uart_hw_init(void) { ...; return 0; } INIT_BOARD_EXPORT(uart_hw_init); ``` 预处理器变成(大意): ```c __attribute__((used, section(".rti_fn.1"))) const init_fn_t __rt_init_uart_hw_init = uart_hw_init; ``` 编译器在 `.rti_fn.1` 里放了一个**函数指针常量**,不是调用 `uart_hw_init`。谁都不用写“把这个函数登记到启动表”——写宏就登记了。开了 `RT_DEBUG_INIT` 时,段里放的是 `{ "函数名", 函数指针 }`,启动日志能打印谁在 init。原理一样。 #### 2. 链接器把各文件的段拼成一张有序表 各 `.c` 里的 `.rti_fn.1`、`.rti_fn.6` 本来是散的。链接脚本(GCC 常见写法): ```ld .rti_fn : { PROVIDE(__rt_init_start = .); KEEP(*(SORT(.rti_fn*))) PROVIDE(__rt_init_end = .); } > CODE ``` `SORT(.rti_fn*)` 按**段名字符串**排序,内存里变成: ``` .rti_fn.0 rti_start ← 内核自己放的哨兵 .rti_fn.0.end rti_board_start .rti_fn.1 ← 所有 INIT_BOARD_EXPORT .rti_fn.1.end rti_board_end .rti_fn.2 INIT_PREV_EXPORT .rti_fn.3 INIT_DEVICE_EXPORT .rti_fn.4 INIT_COMPONENT_EXPORT .rti_fn.5 INIT_ENV_EXPORT .rti_fn.6 ← 所有 INIT_APP_EXPORT .rti_fn.6.end rti_end ``` 这就是一张**按 level 排好的函数指针数组**。同 level 内顺序一般按目标文件/符号名,**不要依赖同级谁先谁后**。 内核在 `components.c` 里自己塞了几个空函数当边界: ```c static int rti_start(void) { return 0; } INIT_EXPORT(rti_start, "0"); static int rti_board_start(void) { return 0; } INIT_EXPORT(rti_board_start, "0.end"); static int rti_board_end(void) { return 0; } INIT_EXPORT(rti_board_end, "1.end"); static int rti_end(void) { return 0; } INIT_EXPORT(rti_end, "6.end"); ``` #### 3. 启动时扫两段,不是扫一整张表 ```c /* 只扫 BOARD:.rti_fn.0.end 到 .rti_fn.1.end(不含 1.end 自己) */ void rt_components_board_init(void) { const init_fn_t *fn; for (fn = &__rt_init_rti_board_start + 1; fn < &__rt_init_rti_board_end; fn++) { (*fn)(); /* 调你的 INIT_BOARD_EXPORT 函数 */ } } /* 扫 PREV ~ APP:.rti_fn.1.end 之后到 .rti_fn.6.end */ void rt_components_init(void) { const init_fn_t *fn; for (fn = &__rt_init_rti_board_end + 1; fn < &__rt_init_rti_end; fn++) { (*fn)(); /* PREV / DEVICE / COMPONENT / ENV / APP */ } } ``` (具体是 `+ 1` 还是开区间,以你这版 `components.c` 为准,意思是:**两个哨兵之间的指针逐个解引用**。) 挂到启动链上: ``` rtthread_startup() 关中断 rt_hw_board_init() 时钟 / 堆 / 串口控制台 / SysTick rt_components_board_init() ← INIT_BOARD_EXPORT 在这里跑 定时器链表、调度器、main/timer/idle 线程 rt_system_scheduler_start() ← 开中断,切到 main 线程 main_thread_entry() rt_components_init() ← INIT_APP_EXPORT 在这里跑 main() ← 你写的 main ``` 所以: - BOARD:还在 `rtthread_startup()` 里,**中断关着、没有当前线程**。 - APP:已经是 `main` 线程,**可以当普通线程代码写**。 和前面“先关中断再 `board_init`”是同一件事:`INIT_BOARD_EXPORT` 跑的时候 `rt_current_thread` 还是空的,一 `rt_thread_delay` / `rt_sem_take` 就会 `rt_schedule()`,当前线程空 → HardFault。`INIT_APP_EXPORT` 跑的时候调度器、tick、堆都在了。 硬件必须先于驱动、驱动必须先于应用,靠的就是 `"1" < "6"` 的段名排序,不是谁 `#include` 谁。 #### 4. 例子 **例 1:板级 —— 注册串口设备(BOARD)** 调度器还没起来,只能碰寄存器、往设备框架里挂对象: ```c #include
#include
static struct rt_serial_device s_uart1; int rt_hw_uart_init(void) { uart1_gpio_clock_init(); s_uart1.ops = &stm32_uart_ops; s_uart1.config.baud_rate = 115200; rt_hw_serial_register(&s_uart1, "uart1", RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX, RT_NULL); return 0; } INIT_BOARD_EXPORT(rt_hw_uart_init); ``` 控制台 `rt_console_set_device("uart1")` 也常在 BOARD 或 `rt_hw_board_init()` 里,这样后面 `rt_kprintf` 才有设备。 **不要**在 BOARD 里: ```c rt_thread_mdelay(10); /* 没有线程,挂 */ rt_thread_create("x", ...); /* 调度器没 start,即使 create 了也不会跑 */ ``` **例 2:应用 —— 自动拉起业务线程(APP)** ```c #include
static void led_entry(void *param) { rt_pin_mode(LED0_PIN, PIN_MODE_OUTPUT); while (1) { rt_pin_write(LED0_PIN, PIN_HIGH); rt_thread_mdelay(200); rt_pin_write(LED0_PIN, PIN_LOW); rt_thread_mdelay(200); } } int led_app_init(void) { rt_thread_t tid; tid = rt_thread_create("led", led_entry, RT_NULL, 512, 20, 10); if (tid == RT_NULL) return -RT_ERROR; rt_thread_startup(tid); return 0; } INIT_APP_EXPORT(led_app_init); ``` 效果:你在 `main()` 里什么都不写,调度起来后 `rt_components_init()` 会调到 `led_app_init()`,LED 线程自己起来。这就是组件“声明一下就自动启动”。 **例 3:同一模块拆两级** 传感器:BOARD 里只保证 I2C 控制器在;APP 里再创建采样线程。 ```c /* drv_i2c.c —— 板级:挂 i2c1 设备 */ int rt_hw_i2c_init(void) { stm32_i2c_register("i2c1"); return 0; } INIT_BOARD_EXPORT(rt_hw_i2c_init); /* app_sht30.c —— 应用:打开设备、建线程 */ int sht30_app_init(void) { rt_device_t i2c = rt_device_find("i2c1"); if (i2c == RT_NULL) return -RT_ENOSYS; rt_thread_t tid = rt_thread_create("sht30", sht30_entry, i2c, 1024, 15, 10); if (tid) rt_thread_startup(tid); return 0; } INIT_APP_EXPORT(sht30_app_init); ``` BOARD 保证 `rt_device_find("i2c1")` 在 APP 时已经有了。如果把 `rt_hw_i2c_init` 也写成 `INIT_APP_EXPORT`,和 `sht30_app_init` 同级,**谁先跑不一定**,`find` 可能失败。 **例 4:中间那几档(知道即可)** | 宏 | level | 典型用途 | | ----------------------- | ----- | -------------------------------------------- | | `INIT_BOARD_EXPORT` | `"1"` | 时钟、GPIO、UART/I2C 控制器注册 | | `INIT_PREV_EXPORT` | `"2"` | 纯软件、不依赖设备(有的版本做对象系统补充) | | `INIT_DEVICE_EXPORT` | `"3"` | 依赖总线的从设备:传感器、Flash | | `INIT_COMPONENT_EXPORT` | `"4"` | DFS、LWIP、USB 协议栈 | | `INIT_ENV_EXPORT` | `"5"` | 文件系统挂载、网络启动 | | `INIT_APP_EXPORT` | `"6"` | 你的业务线程、GUI、协议栈应用 | #### 5. 和 `main()` 的关系 ``` main 线程入口 → rt_components_init() // 所有 APP(以及 2~5)先跑完 → main() // 然后才是你的 main ``` 所以 `INIT_APP_EXPORT(fn)` 比 `main()` **更早**。`main` 里 `rt_device_find("uart1")` 看得到 BOARD 注册的设备;APP 里创建的线程也可能已经在跑。 不要在 BOARD 函数里假设 `main` 已经执行;也不要在 `main` 里再注册一遍 BOARD 已经挂过的设备(除非你没开自动 init)。 #### 6. 容易混的点 1. **宏不“调用”函数,只“投放”指针。** 没被扫到(没开 `RT_USING_COMPONENTS_INIT`、链接脚本没 `KEEP(*(SORT(.rti_fn*)))`),函数永远不会跑,还可能被 GC 掉;所以要 `rt_used`。 2. **同 level 不要排依赖。** I2C 和传感器都 `INIT_APP_EXPORT` 就会偶发 `find` 失败。设备放 BOARD/DEVICE,应用放 APP。 3. **BOARD 里能用堆吗?** 取决于 `rt_system_heap_init()` 是否已经在 `rt_hw_board_init()` 里、并且在 `rt_components_board_init()` **之前**。官方 BSP 一般是先堆再扫 BOARD。不要在 BOARD 里 `rt_thread_create`(动态线程还要调度器)。 4. **和 `MSH_CMD_EXPORT` 是同一套路**:命令表也是 `section` + `KEEP` + 启动/shell 扫描。自动 init 扫的是 `.rti_fn.*`,shell 扫的是另一段。 5. **函数必须全局、签名对。** `static int foo(void)` 可以(符号变成 `__rt_init_foo` 在段里);写成 `void foo(void)` 或带参数,类型和 `init_fn_t` 对不上。 对照前面那句:关中断是为了让 SysTick 先别进 ISR;`INIT_BOARD_EXPORT` 正好跑在这扇门里面——硬件可以配,线程 API 还不能用。`INIT_APP_EXPORT` 跑在门打开之后,就是普通线程里的自动 `main` 前缀。 ```
0
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
_ZHE_
这家伙很懒,什么也没写!
文章
2
回答
0
被采纳
0
关注TA
发私信
相关文章
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
【最新活动】RT-Thread论坛活动正在进行(2026年5月活动已更新)
4
国产MCU移植系列教程汇总,欢迎查看!
5
机器人操作系统 (ROS2) 和 RT-Thread 通信
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
五分钟玩转RT-Thread新社区
8
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
9
STM32的“GPU”——DMA2D实例详解
10
RT-Thread隐藏的宝藏之completion
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
Bootloader
AT
Hardfault
CAN总线
ART-Pi
FinSH
DMA
USB
RT-Thread
SCons
文件系统
RT-Thread Nano
线程
MQTT
STM32
FAL
rt-smart
BSP
cubemx
RTC
WIZnet_W5500
PWM
I2C_IIC
UART
packages_软件包
GD32
ota在线升级
ESP8266
freemodbus
flash
keil_MDK
编译报错
ADC
潘多拉开发板_Pandora
flashDB
Debug
ulog
msh
rt_mq_消息队列_msg_queue
socket
MicroPython
SDIO总线
中断
SFUD
QEMU
本月问答贡献
crystal266
3
个答案
1
次被采纳
CYFS
3
个答案
1
次被采纳
oxlm
1
个答案
1
次被采纳
王棒棒
1
个答案
1
次被采纳
wdfk_prog
1
个答案
1
次被采纳
本月文章贡献
Ewan11111
3
篇文章
1
次点赞
longtsing
1
篇文章
4
次点赞
RTT_Studio
1
篇文章
4
次点赞
枫雪天
1
篇文章
3
次点赞
PVNR_2505
1
篇文章
3
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部