jklincn


Stm32F103 + NuttX(三):系统启动流程分析


在编写自己的板级配置之前,我觉得还是要先了解整个 NuttX 系统的初始化流程,看看每次复位后都运行了哪些函数,这样可以确定我们要编写的最小代码。

本文还是以 stm32f103-minimum 为例。

从链接脚本入手

这里可以看到程序入口是 _stext,这只是一个标记,位于 .text 的开始,而 .text 段以向量表 *(.vectors) 开始。*(.vectors) 是链接脚本中的段选择器,用于收集所有放置在 .vectors 段的代码。

OUTPUT_ARCH(arm)
EXTERN(_vectors)
ENTRY(_stext)
SECTIONS
{
    .text : {
        _stext = ABSOLUTE(.);
        *(.vectors)
        *(.text .text.*)
        *(.fixup)
        *(.gnu.warning)
        *(.rodata .rodata.*)
        *(.gnu.linkonce.t.*)
        *(.glue_7)
        *(.glue_7t)
        *(.got)
        *(.gcc_except_table)
        *(.gnu.linkonce.r.*)
        _etext = ABSOLUTE(.);
    } > flash
    ...
}

全局查找 .vectors 可以发现这是架构通用代码,像 STM32 这种 ARMv7-M 架构的,其内容如下。这个文件定义了 NuttX 在 ARMv7-M 架构下的中断向量表。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// nuttx/arch/arm/src/armv7-m/arm_vectors.c
#include <nuttx/config.h>

#include "chip.h"
#include "arm_internal.h"
#include "ram_vectors.h"
#include "nvic.h"

// 初始堆栈指针,指向 SRAM 的可用区域(BSS 段末尾加上空闲线程的堆栈大小)
#define IDLE_STACK      (_ebss + CONFIG_IDLETHREAD_STACKSIZE)

extern void __start(void);

static void start(void)
{
  // 设置 lr 寄存器为 0,表示这是调用链的起点。然后跳转到 __start 外部函数
  asm volatile ("mov lr, #0\n\t"
                "b  __start\n\t");
}

extern void exception_common(void);
extern void exception_direct(void);

// 使用 locate_data(".vectors") 强制放置在 .vectors 段,确保向量表位于 FLASH 的开头
const void * const _vectors[] locate_data(".vectors")
                              aligned_data(VECTAB_ALIGN) =
{
  // 初始堆栈指针
  IDLE_STACK,
    
  // 重置向量
  start,

  // [2 ... NVIC_IRQ_PENDSV] 指向通用异常处理
  // 其余指向直接中断处理(用于外设中断)
  [2 ... NVIC_IRQ_PENDSV] = &exception_common,
  [(NVIC_IRQ_PENDSV + 1) ... (15 + ARMV7M_PERIPHERAL_INTERRUPTS)]
                          = &exception_direct
};

根据 ,《Armv7-M Architecture Reference Manual》 描述,中断向量表的第一项(偏移量 0)为 SP_main,之后的中断序号中,第一个是 Reset,因此在上述代码的设置下,我们复位后会进入 start 函数,并马上跳转到 __start

复位入口

__start 函数位于 nuttx/arch/arm/src/stm32/stm32_start.c,它负责系统启动前的所有初始化工作,确保硬件和软件环境准备好后启动 NuttX 内核。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#include <nuttx/config.h>

#include <stdint.h>
#include <assert.h>
#include <debug.h>

#include <nuttx/init.h>

#include "arch/board/board.h"
#include "arm_internal.h"
#include "itm_syslog.h"
#include "nvic.h"
#include "mpu.h"

#include "stm32.h"
#include "stm32_gpio.h"
#include "stm32_userspace.h"
#include "stm32_start.h"

// 计算堆的起始地址,从 BSS 结束 + 空闲堆栈大小 开始。
#define HEAP_BASE      ((uintptr_t)_ebss + CONFIG_IDLETHREAD_STACKSIZE)

// 只读变量,提供空闲线程堆栈的顶部地址,即堆的起始地址,内核使用此地址初始化空闲线程的堆栈上下文,确保堆栈不与堆冲突。
const uintptr_t g_idle_topstack = HEAP_BASE;

// 如果启用了 CONFIG_DEBUG_FEATURES,则开启输出
#ifdef CONFIG_DEBUG_FEATURES
#  define showprogress(c) arm_lowputc(c)
#else
#  define showprogress(c)
#endif

// 避免在早期阶段干扰堆栈检查
#ifdef CONFIG_ARMV7M_STACKCHECK
void __start(void) noinstrument_function;
#endif

void __start(void)
{
  const uint32_t *src;
  uint32_t *dest;

  // 如果进行堆栈检查,则设置 r10 为堆栈限制(CONFIG_IDLETHREAD_STACKSIZE - 64),防止堆栈溢出
#ifdef CONFIG_ARMV7M_STACKCHECK
  /* Set the stack limit before we attempt to call any functions */

  __asm__ volatile("sub r10, sp, %0" : :
                   "r"(CONFIG_IDLETHREAD_STACKSIZE - 64) :);
#endif

  // MPU 早期重置
  mpu_early_reset();

  // 时钟配置
  stm32_clockconfig();
  arm_fpuconfig();
  // 初始化用于串口通信的 USART,确保调试输出可用
  stm32_lowsetup();
  stm32_gpioinit();
    
  // DEBUG 阶段性输出,后续同理
  showprogress('A');

  // 清零 BSS 段
  for (dest = (uint32_t *)_START_BSS; dest < (uint32_t *)_END_BSS; )
    {
      *dest++ = 0;
    }

  showprogress('B');

  // 将已初始化的数据段从其在 FLASH 中的临时存放位置移动到 SRAM 中的正确位置
  for (src = (const uint32_t *)_DATA_INIT,
       dest = (uint32_t *)_START_DATA; dest < (uint32_t *)_END_DATA;
      )
    {
      *dest++ = *src++;
    }

  showprogress('C');

  // 可选初始化:堆栈检查初始化
#ifdef CONFIG_ARMV7M_STACKCHECK
  arm_stack_check_init();
#endif

  // 可选初始化:性能事件初始化
#ifdef CONFIG_ARCH_PERF_EVENTS
  up_perf_init((void *)STM32_SYSCLK_FREQUENCY);
#endif

  // 可选初始化:ITM SYSLOG 初始化
#ifdef CONFIG_ARMV7M_ITMSYSLOG
  itm_syslog_initialize();
#endif

  // 可选初始化:早期串口初始化
#ifdef USE_EARLYSERIALINIT
  arm_earlyserialinit();
#endif

  showprogress('D');

  // 对于用户空间/内核空间分离的构建,进行平台特定的用户内存初始化
#ifdef CONFIG_BUILD_PROTECTED
  stm32_userspace();
  showprogress('E');
#endif

  // 板级资源初始化
  stm32_boardinitialize();
  showprogress('F');

  showprogress('\r');
  showprogress('\n');

  // 启动 NuttX 内核
  nx_start();

  // 防御性代码:不应该到达此处,如果 nx_start() 返回了,则进入无限循环

#ifndef CONFIG_DISABLE_IDLE_LOOP
  for (; ; );
#endif
}

板级资源初始化(一)

既然是板级资源,那肯定和板级配置有关,stm32 的启动代码就会跳转到 stm32f103-minimum/src/stm32_boot.c。这里也是根据可选的配置进行分别进行自动 LED、SPI、USB 的初始化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <nuttx/config.h>
#include <nuttx/spi/spi.h>
#include <debug.h>

#include <nuttx/board.h>
#include <arch/board/board.h>

#include "arm_internal.h"
#include "stm32f103_minimum.h"

void stm32_boardinitialize(void)
{
#ifdef CONFIG_ARCH_LEDS
  board_autoled_initialize();
#endif

#if defined(CONFIG_STM32_SPI1) || defined(CONFIG_STM32_SPI2)
  stm32_spidev_initialize();
#endif

#if defined(CONFIG_USBDEV) && defined(CONFIG_STM32_USB)
  stm32_usbinitialize();
#endif
}

内核初始化

nx_start 位于 nuttx/sched/init/nx_start.c,负责进行 NuttX 内核的初始化。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
void nx_start(void)
{
  int i;

  // DEBUG 输出
  sinfo("Entry\n");

  // 初始化状态:已完成 BOOT
  g_nx_initstate = OSINIT_BOOT;

  // 初始化任务列表
  tasklist_initialize();

  // 初始化 IDLE 任务
  idle_task_initialize();

  // 初始化状态:任务列表已就绪
  g_nx_initstate = OSINIT_TASKLISTS;

  // 开始初始化 RTOS 数据

  // 驱动早期初始化,允许一些驱动在内核初始化时使用
  drivers_early_initialize();

  // 开启调度追踪(需要配置 CONFIG_TRACE_SCHED)
  sched_trace_begin();

  // 开始初始化 RTOS 功能
    
  // 初始化信号量,这进行的很早因为后续有很多子系统依赖信号量。
  nxsem_initialize();

  // 初始化内存管理器
#if defined(MM_KERNEL_USRHEAP_INIT) || defined(CONFIG_MM_KERNEL_HEAP) || \
    defined(CONFIG_MM_PGALLOC)
    {
      FAR void *heap_start;
      size_t heap_size;
      // 可选初始化:用户内存分配器
#ifdef MM_KERNEL_USRHEAP_INIT
      up_allocate_heap(&heap_start, &heap_size);
      kumm_initialize(heap_start, heap_size);
#endif
      // 可选初始化:内核内存分配器
#ifdef CONFIG_MM_KERNEL_HEAP
      up_allocate_kheap(&heap_start, &heap_size);
      kmm_initialize(heap_start, heap_size);
#endif
      // 可选初始化:页分配器
#ifdef CONFIG_MM_PGALLOC
      up_allocate_pgheap(&heap_start, &heap_size);
      mm_pginitialize(heap_start, heap_size);
#endif
    }
#endif

  // 可选初始化:内核动态映射模块
#ifdef CONFIG_MM_KMAP
  kmm_map_initialize();
#endif
    
  // 可选初始化:额外的堆
#ifdef CONFIG_ARCH_HAVE_EXTRA_HEAPS
  up_extraheaps_init();
#endif

  // 可选初始化:IO 缓存
#ifdef CONFIG_MM_IOB
  iob_initialize();
#endif

  // 初始化 PID 的哈希表逻辑
  // - 计算初始哈希表大小
  i = 1 << LOG2_CEIL(CONFIG_PID_INITIAL_COUNT);
  while (i <= CONFIG_SMP_NCPUS)
    {
      i <<= 1;
    }
  // - 分配内存空间
  g_pidhash = kmm_zalloc(sizeof(*g_pidhash) * i);
  DEBUGASSERT(g_pidhash);
  // - 保存哈希表大小到全局变量
  g_npidhash = i;

  // 初始化 IDLE 任务组
  idle_group_initialize();

  // 记录最后一个分配的 PID。确保后续新任务的 PID 从 CONFIG_SMP_NCPUS 开始递增,避免与 IDLE PID 冲突。如果有 2 个 CPU,则下一个任务的 PID 为 2
  g_lastpid = CONFIG_SMP_NCPUS - 1;

  // 初始化状态:内存管理已就绪
  g_nx_initstate = OSINIT_MEMORY;

  // 初始化与任务相关的数据结构
  task_initialize();

  // 初始化 instrument 功能,看起来用于调试和性能分析
  instrument_initialize();

  // 初始化文件系统
  fs_initialize();

  // 初始化中断处理系统
  irq_initialize();

  // 初始化 POSIX 标准的时钟功能
  clock_initialize();

  // 可选初始化:定时器
#ifndef CONFIG_DISABLE_POSIX_TIMERS
  timer_initialize();
#endif

  // 初始化信号功能
  nxsig_initialize();

  // 可选初始化:消息队列
#if !defined(CONFIG_DISABLE_MQUEUE) || !defined(CONFIG_DISABLE_MQUEUE_SYSV)
  nxmq_initialize();
#endif

  // 可选初始化:网络
#ifdef CONFIG_NET
  net_initialize();
#endif

  // 可选初始化:二进制格式
#ifndef CONFIG_BINFMT_DISABLE
  binfmt_initialize();
#endif

  // 开始初始化硬件功能

  // 处理器架构特定的初始化函数,比如中断服务例程和时钟启动
  up_initialize();

  // 初始化通用驱动
  drivers_initialize();

  // 可选初始化:板级早期初始化
#ifdef CONFIG_BOARD_EARLY_INITIALIZE
  board_early_initialize();
#endif

  // 初始化状态:硬件已可用
  g_nx_initstate = OSINIT_HARDWARE;

  // 开始设置多任务

  // 记录 CPU0 IDLE 任务启动事件,用于调度跟踪和调试
  sched_note_start(&g_idletcb[0]);

  // 为每个 CPU 的 IDLE 任务初始化 stdio
  for (i = 0; i < CONFIG_SMP_NCPUS; i++)
    {
      if (i > 0)
        {
          // 对于非 CPU0 的 IDLE 任务,从 CPU0 克隆文件描述符
          DEBUGVERIFY(group_setuptaskfiles(&g_idletcb[i], NULL, true));
        }
      else
        {
          // 对于 CPU0,创建初始 stdio 文件。这些文件将被后续任务继承。
          DEBUGVERIFY(group_setupidlefiles());
        }
    }

  // SMP CPU 启动
#ifdef CONFIG_SMP
  // 断言当前是 CPU0
  DEBUGASSERT(this_cpu() == 0);
  // 启动其他 CPU,初始化多核环境
  DEBUGVERIFY(nx_smp_start());
#endif

  // 启动系统

  // 初始化状态:OS 已就绪,开始多任务
  g_nx_initstate = OSINIT_OSREADY;

  // 启动初始任务
  DEBUGVERIFY(nx_bringup());

  // 进入 IDLE 任务循环
    
  // 初始化状态:空闲循环
  g_nx_initstate = OSINIT_IDLELOOP;

  // 结束调度跟踪
  sched_trace_end();
    
  // 解锁调度器,允许其他线程访问内存管理器
  sched_unlock();

  // IDLE 循环

  sinfo("CPU0: Beginning Idle Loop\n");
#ifndef CONFIG_DISABLE_IDLE_LOOP
  for (; ; )
    {
      // 调用处理器特定的空闲操作(如低功耗模式),节省能源
      up_idle();
    }
#endif
}

这里进行了一系列的内核子系统初始化,创建了 IDLE 任务,最后调用 nx_bringup() 启动了初始任务。

初始任务

nx_bringup 的主要作用是在操作系统基本初始化完成后,启动系统级的初始任务和服务,包括页面管理、工作队列、文件系统挂载以及应用程序的启动。该文件确保系统从内核模式过渡到用户模式,并为后续的用户应用程序提供运行环境。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int nx_bringup(void)
{
  // 开始调度跟踪
  sched_trace_begin();

  // 为 IDLE 任务设置初始环境变量,如 PWD、PATH 和 LD_LIBRARY_PATH
#ifndef CONFIG_DISABLE_ENVIRON

#ifdef CONFIG_LIBC_HOMEDIR
  setenv("PWD", CONFIG_LIBC_HOMEDIR, 1);
#endif

#ifdef CONFIG_PATH_INITIAL
  setenv("PATH", CONFIG_PATH_INITIAL, 1);
#endif

#ifdef CONFIG_LDPATH_INITIAL
  setenv("LD_LIBRARY_PATH", CONFIG_LDPATH_INITIAL, 1);
#endif
#endif

  // 启动页面填充内核线程(处理缺页异常)
  nx_pgworker();

  // 启动工作队列线程
  nx_workqueues();

  // 执行板级初始化(late)并启动应用初始化线程
  nx_create_initthread();

  // 调度跟踪结束
  sched_trace_end();
  return OK;
}

由于当前配置没有设置 CONFIG_BOARD_LATE_INITIALIZE,因此直接就是在当前线程中继续进行板级和应用的初始化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
static inline void nx_create_initthread(void)
{
#ifdef CONFIG_BOARD_LATE_INITIALIZE
  int pid;

  /* Do the board/application initialization on a separate thread of
   * execution.
   */

  pid = nxthread_create("AppBringUp", TCB_FLAG_TTYPE_KERNEL,
                        CONFIG_BOARD_INITTHREAD_PRIORITY,
                        NULL, CONFIG_BOARD_INITTHREAD_STACKSIZE,
                        nx_start_task, NULL, environ);
  DEBUGASSERT(pid > 0);
  UNUSED(pid);
#else
  /* Do the board/application initialization on this thread of execution. */

  nx_start_application();
#endif
}

创建入口点

nx_start_application 会完成最后的板级设置,然后根据配置(通过入口点或文件路径)启动用户应用程序。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
static inline void nx_start_application(void)
{
  // 可选:准备参数数组
#ifndef CONFIG_INIT_NONE
  FAR char * const argv[] =
  {
#  ifdef CONFIG_INIT_ARGS
    CONFIG_INIT_ARGS,
#  endif
    NULL,
  };

  posix_spawnattr_t attr;
#endif
  int ret;

  // 可选:挂载 ROMFS 到 /etc
#ifdef CONFIG_ETC_ROMFS
  nx_romfsetc();
#endif

  // 可选:进行板级晚期初始化。
#ifdef CONFIG_BOARD_LATE_INITIALIZE
  board_late_initialize();
#endif

  // 可选:核心转储初始化
#ifdef CONFIG_COREDUMP
  coredump_initialize();
#endif

  // 如果定义 CONFIG_INIT_NONE,记录日志并跳过(适用于flat build)
#ifdef CONFIG_INIT_NONE
  UNUSED(ret);
  sinfo("No init thread\n");
#else
  // 否则,初始化 posix_spawnattr_t 属性(优先级和栈大小)
  posix_spawnattr_init(&attr);
  attr.priority  = CONFIG_INIT_PRIORITY;
  attr.stacksize = CONFIG_INIT_STACKSIZE;

  // 启动配置 1:入口点
#if defined(CONFIG_INIT_ENTRY)

  sinfo("Starting init thread\n");

#  ifdef CONFIG_BUILD_PROTECTED
  DEBUGASSERT(USERSPACE->us_entrypoint != NULL);
  // - 1.1 Protected 构建从 USERSPACE->us_entrypoint 获取入口点
  ret = task_spawn(CONFIG_INIT_ENTRYNAME,
                   USERSPACE->us_entrypoint,
                   NULL, &attr, argv, NULL);
#  else
  // - 1.2 Flat 构建从 CONFIG_INIT_ENTRYPOINT 获取入口点
  ret = task_spawn(CONFIG_INIT_ENTRYNAME,
                   CONFIG_INIT_ENTRYPOINT,
                   NULL, &attr, argv, NULL);
#  endif
#elif defined(CONFIG_INIT_FILE)

  // 启动配置 2:文件
#  ifdef CONFIG_INIT_MOUNT
  // 挂载文件系统
  ret = nx_mount(CONFIG_INIT_MOUNT_SOURCE, CONFIG_INIT_MOUNT_TARGET,
                 CONFIG_INIT_MOUNT_FSTYPE, CONFIG_INIT_MOUNT_FLAGS,
                 CONFIG_INIT_MOUNT_DATA);
  DEBUGASSERT(ret >= 0);
#  endif

  sinfo("Starting init task: %s\n", CONFIG_INIT_FILEPATH);

  posix_spawnattr_init(&attr);

  attr.priority  = CONFIG_INIT_PRIORITY;
  attr.stacksize = CONFIG_INIT_STACKSIZE;
  // 从文件执行程序。
  ret = exec_spawn(CONFIG_INIT_FILEPATH, argv, NULL,
                   CONFIG_INIT_SYMTAB, CONFIG_INIT_NEXPORTS, NULL, &attr);
#endif
  posix_spawnattr_destroy(&attr);
  DEBUGASSERT(ret > 0);
#endif /* CONFIG_INIT_NONE */
}

这里会涉及到 Flat 构建和 Protected 构建:

  • Flat 构建:内核和用户应用程序共享同一个地址空间,没有内存保护分离
    • 所有代码(内核和用户)运行在同一权限级别。
    • 内存访问无限制,用户代码可直接访问内核内存。
    • 启动简单,性能开销低,适用于资源受限的嵌入式系统。
    • 安全性低:用户代码错误可能导致整个系统崩溃。
  • Protected 构建:内核和用户空间分离,提供基本的内存保护。
    • 内核运行在特权模式,用户应用程序在非特权模式。
    • 用户空间无法直接访问内核内存,系统调用通过接口交互。
    • 安全性较高:用户代码错误不会直接影响内核。
    • 启动更复杂,需要用户空间 blob(二进制块)。

一般嵌入式设备的话选用 Flat 构建就可以了,我们这里看 Flat 构建的分支就是通过 CONFIG_INIT_ENTRYPOINT 创建任务。在 stm32f103-minimum:nsh 的配置下,

CONFIG_INIT_ENTRYPOINT="nsh_main"

启动 nsh

目前内核所有的初始化工作都已经完成了,马上要启动 nsh,从上面代码可以看到是通过 CONFIG_INIT_ENTRYPOINT 来控制入口点,当前配置下这个值是 nsh_main,那 nuttx 内核要怎么找到这个 app 函数呢?

这就需要看 cmake 的相关文件,有一个非常重要的宏定义 nuttx_add_application,位于 nuttx/cmake/nuttx_add_application.cmake,有 200+ 行代码,这里挑出最关键的部分,其余的可以自行查看与学习。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// nuttx/cmake/nuttx_add_application.cmake:153
# 将应用程序作为静态库 libapps.a 链接到内核
set(TARGET "apps_${NAME}")
add_library(${TARGET} ${SRCS})

...

if(NOT NO_MAIN_ALIAS)
# 从 SRCS(源文件列表)中获取第一个文件,并赋值给 MAIN_SRC。
list(GET SRCS 0 MAIN_SRC)
# 入口点映射
# 为指定的源文件(${MAIN_SRC})添加编译定义(compile definition):main=${NAME}_main。
# 这会在编译时通过 -Dmain=nsh_main(对于NAME=nsh)传递给编译器。
# 结果:nsh_main.c 中的 int main(...) 被宏替换为 int nsh_main(...),创建别名。
set_property(
  SOURCE ${MAIN_SRC}
  APPEND
  PROPERTY COMPILE_DEFINITIONS main=${NAME}_main)
endif()

通过映射之后,内核就开始运行 apps/system/nsh/nsh_main.c 中的 main 函数了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int main(int argc, FAR char *argv[])
{
  struct sched_param param;
  int ret = 0;

  /* Check the task priority that we were started with */

  sched_getparam(0, &param);
  if (param.sched_priority != CONFIG_SYSTEM_NSH_PRIORITY)
    {
      /* If not then set the priority to the configured priority */

      param.sched_priority = CONFIG_SYSTEM_NSH_PRIORITY;
      sched_setparam(0, &param);
    }

  /* Initialize the NSH library */

  nsh_initialize();

#ifdef CONFIG_NSH_CONSOLE
  /* If the serial console front end is selected, run it on this thread */

  ret = nsh_consolemain(argc, argv);

  /* nsh_consolemain() should not return.  So if we get here, something
   * is wrong.
   */

  dprintf(STDERR_FILENO, "ERROR: nsh_consolemain() returned: %d\n", ret);
  ret = 1;
#endif

  return ret;
}

但这并不是初始化的结束。

板级资源初始化(二)

板级初始化一个非常重要的函数 stm32_bringup 到现在都还没被调用,我们接着看 nsh_initialize

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void nsh_initialize(void)
{
  ...

#ifdef CONFIG_NSH_ARCHINIT
  /* Perform architecture-specific initialization (if configured) */

  boardctl(BOARDIOC_INIT, 0);
#endif

  ...
}

由于默认配置了 CONFIG_NSH_ARCHINIT,因此这里会调用 boardctl

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
int boardctl(unsigned int cmd, uintptr_t arg)
{
  int ret = OK;

  switch (cmd)
    {
      case BOARDIOC_INIT:
        {
          ret = board_app_initialize(arg);
        }
        break;
      ...
      
  if (ret < 0)
  {
    set_errno(-ret);
    return ERROR;
  }

  return ret;
}

我们只看 BOARDIOC_INIT 分支,发现其调用了 board_app_initialize,这是板级代码,位于 stm32_appinit.c 中。

1
2
3
4
5
6
int board_app_initialize(uintptr_t arg)
{
  /* Perform board initialization here */

  return stm32_bringup();
}

它会再调用 stm32_bringup 进行初始化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// stm32f103-minimum/src/stm32_bringup.c
int stm32_bringup(void)
{
#ifdef CONFIG_ONESHOT
  struct oneshot_lowerhalf_s *os = NULL;
#endif
  int ret = OK;

#ifdef CONFIG_DEV_GPIO
  ret = stm32_gpio_initialize();
  if (ret < 0)
    {
      syslog(LOG_ERR, "Failed to initialize GPIO Driver: %d\n", ret);
      return ret;
    }
#endif

#ifdef CONFIG_VIDEO_FB
  /* Initialize and register the framebuffer driver */

  ret = fb_register(0, 0);
  if (ret < 0)
    {
      syslog(LOG_ERR, "ERROR: fb_register() failed: %d\n", ret);
    }
#endif

  ...

#ifdef CONFIG_USERLED
  /* Register the LED driver */

  ret = userled_lower_initialize("/dev/userleds");
  if (ret < 0)
    {
      syslog(LOG_ERR, "ERROR: userled_lower_initialize() failed: %d\n", ret);
    }
#endif

  ...

  return ret;
}

在这里面我们可以看到具体的板级资源的初始化,代码比较长,因此做了一些省略,主要就是根据配置是否打开来进行对应的初始化,在 nsh 的默认配置下,大部分外设资源都没有启用。

至此,全部的初始化才完成,下一步就是 nsh 打开其控制台 nsh_consolemain()

总结

这里我们碰到了 4 个板级资源初始化,分别是

  • BOOT 阶段(__start):调用 stm32_boardinitialize() 进行最早的初始化
  • 内核初始化阶段(nx_start):如果有配置,会调用 board_early_initialize 进行板级早期初始化
  • 系统初始任务启动阶段(nx_start_task):如果有配置,调用 board_late_initialize 进行板级晚期初始化。
  • nsh 应用启动阶段(nsh_initialize):调用 board_app_initialize 进行应用级的板级初始化。

除两个需要额外配置的 hook 类函数外,介绍一下 stm32_boardinitializeboard_app_initialize 的区别:

  • stm32_boardinitialize 是内核级初始化,其调用时机非常早,其目的是初始化那些必须在内核启动前就绪的驱动(如 LED、某些总线)。它的限制是不能使用任何依赖于操作系统服务的功能(如信号量、互斥锁、文件系统操作、动态内存分配等),因为 OS 还没完全跑起来。
  • board_app_initialize 是应用级初始化,调用时机相对较晚,在系统完全启动后,由第一个用户应用程序(通常是 NSH)通过 boardctl(BOARDIOC_INIT) 系统调用触发。其目的是初始化应用层需要的板级资源,注册设备驱动,挂载文件系统,初始化网络协议栈等。其优势是可以自由使用所有的操作系统服务(锁、内存分配、打印调试信息等),非常适合做复杂的设备初始化。


本站不记录浏览量,但如果您觉得本内容有帮助,请点个小红心,让我知道您的喜欢。