NuttX整体架构

翻译自 https://raw.githubusercontent.com/engehcall/technology/master/NuttX/nuttx-overview.pdf

1. NuttX的整体架构

1.1 什么是实时操作系统(RTOS)?

1.1.1 作为库的RTOS

描述
NuttX和其他RTOS一样,是一组功能的集合,以库的形式提供。
它仅在以下情况下运行:

  1. 应用程序调用NuttX库代码;
  2. 发生中断。

架构表示
对于以用户管理函数库形式实现的架构,没有直观的方式可以通过图表来表示。
但是,可以选择RTOS的任意子系统,并以某种方式对其进行表示。

1.1.2 内核线程

有一些RTOS功能是通过内部线程实现的[待补充]。

1.2 调度器

1.2.1 调度器与操作系统

操作系统的定义
操作系统是为开发应用程序而设计的完整环境。
操作系统的一个重要组成部分是调度器:控制任务或线程执行的逻辑。
实际上,调度器的功能远不止于此,它还决定了什么是任务或线程!

小型操作系统的特点
一些小型操作系统(如FreeRTOS)实际上并不提供完整的操作系统环境,而仅仅包含调度器。
这也体现了调度器的重要性。

1.2.2 任务控制块(TCB)

定义
在NuttX中,线程是指具有自己栈的任何可控指令执行序列。
每个任务由一个称为任务控制块(Task Control Block, TCB)的数据结构表示。

存储位置
TCB数据结构定义在头文件 include/nuttx/sched.h 中。

1.2.3 任务列表

任务状态与列表的关系
TCB通过 task_state 字段和一系列任务列表来表示任务的状态。
虽然并非所有列表都需要优先级,但大多数列表是优先级化的,以便使用通用的列表处理逻辑。
需要优先级化的列表包括:
g_readytorun(就绪运行列表)
g_pendingtasks(挂起任务列表)
g_waitingforsemaphore(等待信号量列表)

任务列表的详细说明

  1. 未激活任务列表

    volatile dq_queue_t g_inactivetasks;
    

    这是所有已初始化但尚未激活的任务列表。注意:这是唯一一个非优先级化的列表。

  2. 就绪运行任务列表

    volatile dq_queue_t g_readytorun;
    

    这是所有准备运行的任务列表。
    ◦ 列表的头部是当前正在运行的任务;
    ◦ 列表的尾部是空闲任务。

  3. 挂起任务列表

    volatile dq_queue_t g_pendingtasks;
    

    这是所有准备运行但因以下原因未被放入 g_readytorun 列表的任务:
    ◦ 它们的优先级高于 g_readytorun 列表头部的当前运行任务;
    ◦ 当前运行任务禁用了抢占。
    这些任务会一直停留在该列表中,直到抢占重新启用,或者当前任务主动释放CPU。

  4. 阻塞任务列表
    当任务进入阻塞状态时,其TCB会被移动到以下阻塞列表之一:
    等待信号量

    volatile dq_queue_t g_waitingforsemaphore;
    

    等待信号(仅在未禁用信号支持时存在):

    volatile dq_queue_t g_waitingforsignal;
    

    等待消息队列非空(仅在未禁用消息队列支持时存在):

    volatile dq_queue_t g_waitingformqnotempty;
    

    等待消息队列非满(仅在未禁用消息队列支持时存在):

    volatile dq_queue_t g_waitingformqnotfull;
    

    等待页面填充(仅在选择按需分页时存在):

    volatile dq_queue_t g_waitingforfill;
    

1.2.4 状态转换图

• 线程的状态可以通过简单的状态转换图表示。
图示待补充

1.2.5 调度策略

实时调度策略
为了成为实时操作系统(RTOS),必须支持 SCHED_FIFO(严格优先级调度)。
• 优先级最高的线程总是运行;
• 优先级最高的线程始终与 g_readytorun 列表头部的TCB关联。

NuttX支持的额外调度策略
SCHED_RR(时间片轮转调度):
• 如果任务使用 SCHED_RR 调度策略运行,则每个时间片结束时,它会将CPU让给同一优先级的下一个任务。
• 注意:
1. 如果只有一个任务在该优先级,则 SCHED_RRSCHED_FIFO 的行为相同;
2. SCHED_FIFO 任务永远不会以这种方式被抢占。

1.2.6 任务ID

任务标识
每个任务不仅由TCB表示,还由一个数字任务ID表示。
• 给定任务ID,RTOS可以找到对应的TCB;
• 给定TCB,RTOS可以找到对应的任务ID。

接口暴露
在RTOS/应用程序接口中,仅暴露任务ID,而不直接暴露TCB。

1.3 NuttX 任务

1.3.1 进程与线程

在像 Windows 或 Linux 这样的较大系统操作系统中,经常会听到“进程”这个词,它指的是由操作系统管理的线程。进程比我们迄今为止讨论的线程更复杂。进程是一个受保护的环境,可以托管一个或多个线程。这里所说的“环境”是指操作系统分配的一组资源,但对于进程的受保护环境,我们特指其地址空间。

为了实现进程的地址空间,CPU 必须支持内存管理单元(MMU)。MMU 用于强制执行受保护的进程环境。

然而,NuttX 是为支持资源受限、低端、深度嵌入式的微控制器(MCU)而设计的。这些 MCU 很少有 MMU,因此无法支持像 Windows 和 Linux 那样的进程。所以 NuttX 不支持进程。NuttX 虽然支持 MMU,但不会使用 MMU 来支持进程。NuttX 仅在平坦地址空间中运行。(NuttX 会使用 MMU 来控制指令和数据缓存,并支持受保护的内存区域。)

1.3.2 NuttX 任务与任务资源

所有实时操作系统(RTOS)都支持“任务”的概念。任务是 RTOS 中与进程相对应的概念。像进程一样,任务是一个带有环境的线程。这个环境类似于进程的环境,但不包括私有地址空间。这个环境是私有的且对每个任务来说是唯一的。每个任务都有自己的环境。

任务环境由多个资源组成(如任务控制块 TCB 中所示)。以下是讨论中感兴趣的资源(注意,这些任务资源可以在 NuttX 配置中被禁用以减少内存占用):

  1. 环境变量:形如 VARIABLE=VALUE 的变量集合。
  2. 文件描述符:任务特定的数字,表示打开的资源(例如文件或设备驱动程序)。
  3. 套接字:套接字描述符类似于文件描述符,但这里打开的资源是网络套接字。
  4. :流表示标准的 C 缓冲 I/O。流包装了文件描述符或套接字,并提供了一组新的接口函数来处理标准 C I/O(如 fprintf()fwrite() 等)。

在 NuttX 中,任务是通过 task_create() 接口创建的。参考:NuttX 用户指南

1.3.3 伪文件系统与设备驱动

关于 NuttX 文件系统的完整讨论属于其他内容。但为了讨论任务资源,我们也需要对 NuttX 文件系统有一些了解。

NuttX 实现了一个虚拟文件系统(VFS),可以通过标准的 open()close()read()write() 等接口与多个不同的实体进行通信。与其他 VFS 一样,NuttX VFS 支持文件系统挂载点、文件、目录、设备驱动等。

此外,与其他 VFS 一样,NuttX 文件系统也支持伪文件系统,即看起来像正常介质但实际上是在程序控制下呈现的文件系统。例如,在 Linux 中,有 /proc/sys 伪文件系统。伪文件系统没有底层物理介质。

NuttX 的根文件系统始终是一个伪文件系统。这与 Linux 正好相反。在 Linux 中,根文件系统必须始终是某个物理块设备(即使只是一个 initrd RAM 磁盘)。一旦挂载了物理根文件系统,就可以挂载其他文件系统——包括 Linux 伪文件系统,如 /proc/sys。而在 NuttX 中,根文件系统始终是一个不需要任何底层块驱动或物理设备的伪文件系统。然后可以在伪文件系统中挂载真实的文件系统。

这种安排为小型嵌入式世界带来了极大的便利(但也有一些限制——比如文件系统的挂载位置)。

NuttX 通过设备驱动与设备进行交互,即通过控制硬件并符合某些 NuttX 约定的软件(见 include/nuttx/fs/fs.h)。设备驱动在伪文件系统中由设备节点表示。按照惯例,这些设备节点创建在 /dev 目录中。

现在我们已经稍微偏离主题介绍了 NuttX 文件系统和设备节点,可以回到任务资源的讨论。

1.3.4 /dev/console 和标准流

有三种特殊的 I/O 情况:stdinstdoutstderr。这些是 FILE* 类型,分别对应文件描述符 0、1 和 2。当创建第一个线程(称为 IDLE 线程)时,会打开特殊的设备节点 /dev/console/dev/console 为初始任务提供 stdinstdoutstderr

1.3.5 任务环境的继承与 I/O 重定向

当一个任务创建新任务时,新任务会继承其父任务的任务资源。这包括所有环境变量、文件描述符和套接字(注意:这种继承可以通过 NuttX 配置中的特殊选项进行限制)。

因此,如果没有进行特殊操作,每个任务都会使用 /dev/console 进行标准 I/O。然而,任务可以关闭文件描述符 0 到 2 并打开一个新的设备用于标准 I/O。然后创建的任何子任务也会继承该新的重定向的标准 I/O。这种机制在 NuttX 中被广泛使用。例如:
• 在 THTTPD 服务器中,将套接字 I/O 重定向到 CGI 任务的标准 I/O。
• 在 Telnet 服务器中,使新任务继承 Telnet 会话。

1.3.6 任务与 POSIX 线程(Pthreads)

像 Linux 这样的系统也支持 POSIX pthreads。在 Linux 环境中,进程创建时会有一个线程在其中运行。但通过使用 pthread_create() 等接口,可以创建多个线程,这些线程运行并共享相同的进程资源。

NuttX 也支持 POSIX pthreads,并且 NuttX 的 pthreads 也支持这种行为。也就是说,NuttX 的 POSIX pthreads 也共享父任务资源。然而,由于 NuttX 不支持进程地址环境,因此差异并不明显。当一个任务创建 pthread 时,新创建的 pthread 将共享父任务的环境变量、文件描述符、套接字和流。

注意:这些任务资源是引用计数的,只要任务组中的线程仍然处于活动状态,它们就会持续存在。

1.3.7 进程 ID/任务 ID/Pthread ID

“进程 ID”是一个标准术语(通常缩写为 pid),用于标识 NuttX 中的任务。因此,更技术地说,这个数字是上面描述的任务 ID。Pthreads 也由 pthread_t ID 描述。在 NuttX 中,pthread_t ID 也是相同的任务 ID。

2. NuttX 初始化序列

2.1 概述

从最高层次来看,NuttX 的初始化序列可以分为三个阶段:

  1. 硬件特定的上电复位初始化
  2. NuttX RTOS 初始化
  3. 应用程序初始化

这个初始化序列非常简单,因为系统在启动应用程序之前一直以单线程模式运行。这意味着初始化序列只是一系列简单的线性函数调用。在启动应用程序之前,系统会进入多线程模式,情况会变得更加复杂。

每个阶段将在以下段落中详细讨论。

2.2 上电复位初始化

2.2.1 概述

当处理器复位时,软件开始执行。这通常发生在上电时,但所有复位(无论是上电、按下复位按钮还是看门狗定时器超时)基本上都是相同的。处理器复位时执行的软件特定于特定的 CPU 架构,并不是 NuttX 的通用部分。架构特定的复位处理需要完成的工作包括:

  1. 将处理器置于其运行状态。这可能包括设置 CPU 模式、初始化协处理器等。
  2. 设置时钟,以便软件和外设按预期运行。
  3. 设置 C 栈指针(和其他处理器寄存器)。
  4. 初始化内存。
  5. 启动 NuttX。

2.2.2 内存初始化

在 C 语言实现中,变量存储通常分为两类:

  1. 已初始化变量:例如全局变量 x

    int x = 5;
    

    C 代码必须确保复位后,变量 x 的值为 5。这类已初始化变量保存在一个名为 .data 的特殊内存段中。

  2. 未初始化变量:例如全局变量 y

    int y;
    

    但 C 代码仍期望 y 有一个初始值,这个初始值为零。所有这类未初始化变量的值均为零,它们保存在一个名为 .bss 的段中。

当我们说复位处理逻辑初始化内存时,指的是两件事:

  1. 通过将值从 FLASH 复制到 .data 段,提供已初始化变量的初始值。
  2. 将所有未初始化变量重置为零,清空 .bss 段。

2.2.3 STM32 F4 复位

让我们以一个特定的处理器为例,详细了解复位序列。以下是 NuttX 针对 STM32 F4 MCU 的初始化过程。复位逻辑可以在以下两个文件中找到:

  1. nuttx/arch/arm/src/stm32_vectors.S
  2. nuttx/arch/arm/src/stm32_start.c

nuttx/arch/arm/src/stm32_vectors.S

stm32_vectors.S 在复位序列中的作用非常小。该文件提供了所有 STM32 异常向量,而上电复位只是另一个异常向量。需要注意的一些重要事项:

  1. .section .vectors, "ax"
    这个伪操作将所有向量放入一个名为 .vectors 的特殊段中。在 STM32 F4 的链接脚本(位于 nuttx/configs/stm3240g-eval/nsh/ld.script)中可以看到,.vectors 段被强制放置在 FLASH 存储器的最开始位置。

    STM32 F4 可以通过引脚配置以不同方式启动。如果配置为从 FLASH 启动,则在复位时,STM32 的 FLASH 存储器会被映射到地址 0x0000 0000,这就是上电复位中断向量的地址。

  2. 向量表中的前两个 32 位条目
    表示上电异常向量(我们知道在复位时它位于地址 0x0000 0000)。这两个条目是:

    .word IDLE_STACK   /* 向量 0:复位栈指针 */
    .word __start      /* 向量 1:复位向量 */
    

    Cortex-M 系列处理复位向量的方式是独特的。注意这里有两个值:启动线程(IDLE 线程)的栈指针和 IDLE 线程的入口点。当复位发生时,栈指针会自动设置为第一个值,然后处理器跳转到第二个条目中指定的复位入口点 __start。这意味着复位异常处理代码可以用 C 语言而不是汇编语言实现。

nuttx/arch/arm/src/stm32_start.c

复位向量 __start 位于 stm32_start.c 文件中,执行实际的低层架构特定初始化。初始化包括:

stm32_clockconfig();      // 初始化板所需的 PLL 和外设时钟
stm32_fpuconfig();        // 如果初始化 STM32 F4 的硬件浮点,则配置 FPU 并启用访问
stm32_lowsetup();         // 启用低级 UART,以便尽早输出串口调试信息
stm32_gpioinit();         // 执行必要的 GPIO 重映射(F4 是存根,但 F1 系列需要此步骤)
showprogress('A');        // 如果启用了 CONFIG_DEBUG,则在串口控制台输出字符 'A'

接下来初始化内存:

  1. .bss 段被清零(如果启用了 CONFIG_DEBUG,则输出字符 'B')。
  2. .data 段被设置为其初始值(如果启用了 CONFIG_DEBUG,则输出字符 'C')。

然后初始化板级特定逻辑:

stm32_boardinitialize();  // 该函数位于板级特定逻辑中

对于 STM3240G-EVAL 板,stm32_boardinitialize() 执行以下操作:

stm32_spiinitialize();    // 如果启用了 SPI,则初始化 SPI 片选
stm32_selectsram();       // 如果启用了外部 SRAM 支持,则配置 STM32 FSMC
up_ledinit();             // 如果使用了板载 LED,则初始化它们

stm32_boardinitialize() 返回到 __start() 时,低层架构特定的初始化完成,NuttX 启动:

os_start();  // NuttX 入口点,执行 RTOS 特定的下一阶段初始化并启动应用程序

os_start() 执行的操作将在下一段中讨论。

2.3 NuttX RTOS 初始化

2.3.1 os_start()

当低层架构特定的初始化完成后,NuttX 通过调用函数 os_start() 启动。该函数位于文件 nuttx/sched/os_start.c 中。os_start() 执行的操作如下:

注意:这些功能中的许多可以通过 NuttX 配置文件禁用,如果禁用,则不会执行这些操作。

  1. 初始化一些 NuttX 全局数据结构。
  2. 初始化 IDLE 线程的 TCB(即执行初始化的线程)。
  3. kmm_initialize():初始化内存管理器(在大多数配置中,kmm_initialize() 是通用 mm_initialize() 的别名)。
  4. irq_initialize():初始化中断处理子系统。这仅初始化数据结构;CPU 中断仍然被禁用。
  5. wd_initialize():初始化 NuttX 看门狗定时器功能。
  6. clock_initialize():初始化系统时钟。
  7. timer_initialize():初始化 POSIX 定时器功能。
  8. sig_initialize():初始化 POSIX 信号功能。
  9. sem_initialize():初始化 POSIX 信号量功能。
  10. mq_initialize():初始化 POSIX 消息队列功能。
  11. pthread_initialize():初始化 POSIX pthread 功能。
  12. fs_initialize():初始化文件系统功能。
  13. net_initialize():初始化网络功能。

到此为止,所有初始化步骤都只是软件初始化。没有任何操作与硬件交互。这些步骤只是为中断和线程等功能的正常运行准备环境。接下来的阶段依赖于这些设置。

  1. up_initialize():处理运行操作系统的处理器特定细节。例如设置中断服务例程和启动时钟等。这些操作因处理器和硬件平台而异。以下是 ARM 版本此函数执行的初始化步骤的具体示例。
  2. lib_initialize():初始化 C 库。这是最后一步,因为库可能依赖于上述初始化。
  3. sched_setupidlefiles():打开 /dev/console 并为 IDLE 线程创建 stdinstdoutstderr。IDLE 线程后续创建的所有任务都会继承这些文件描述符。
  4. os_bringup():创建初始任务。具体描述见下文。
  5. 最后进入 IDLE 循环。初始化完成后,IDLE 线程的角色发生变化。它现在成为一个仅在系统中没有其他任务可运行时才执行的线程(因此称为 IDLE 线程)。

2.3.2 IDLE 线程活动

如前所述,IDLE 线程是仅在系统中没有其他任务可运行时才执行的线程。它在系统中具有最低优先级,始终为优先级 0。它是唯一允许具有优先级 0 的线程,并且永远不会被阻塞(否则将没有线程可以运行)。

因此,IDLE 线程始终位于 g_readytorun 列表中,并且由于该列表是优先级排序的,因此它始终保证是 g_readytorun 列表的最后一个条目。

IDLE 线程是一个无限循环。但这并不会使其成为“CPU 占用大户”。由于它具有最低优先级,因此当其他任务需要运行时,它可以被挂起。

IDLE 线程在这个无限循环中执行以下两件事:

  1. 内存清理:如果工作线程未启动(见下文 os_bringup()),则 IDLE 线程将执行内存清理。内存清理是为了处理延迟内存释放。当内存在一个无法访问堆的上下文中被释放时(例如在中断处理程序中),必须延迟内存分配的释放。在这种情况下,内存会被放入一个已释放内存列表中,并最终由 IDLE 线程清理。
    注意:工作线程的主要功能是作为扩展设备驱动处理的“下半部”。如果工作线程已启动,则它将以比 IDLE 线程更高的优先级运行。在这种情况下,工作线程将接管这些延迟分配的清理责任。
  2. 调用 up_idle():然后循环调用 up_idle()up_idle() 执行的操作是架构和板级特定的。通常,这是执行 CPU 特定低功耗操作的位置。

2.3.3 os_bringup()

os_bringup() 函数在 os_start() 的初始化序列的最后一步调用,就在进入 IDLE 循环之前。该函数位于 nuttx/sched/os_bringup.c 中。它启动系统所需的所有线程和任务。具体执行以下操作:

  1. 如果配置了按需分页,则此函数将启动页面填充任务。这是在具有 MMU 并启用了按需分页的处理器中运行以处理页面错误的任务。
  2. 如果配置了工作线程,则此函数启动工作线程。工作线程可用于执行通过 include/nuttx/wqueue.h 提供的 API 延迟到工作线程的任何处理。工作线程的主要功能是作为扩展设备驱动处理的“下半部”,但可用于多种用途。
  3. 最后,os_bringup() 将启动应用程序任务。默认情况下,这是入口点名为 user_start() 的任务。user_start() 由应用程序代码提供,当它运行时,它开始初始化序列的应用程序特定阶段。

注意:默认的 user_start() 入口点可以更改为使用 NSH 提供的命名应用程序之一。这是一个不常用的启动选项,此处不再进一步讨论。

2.3.4 STM32 F4 up_initialize()

所有基于 ARM 的 MCU 都共享一个通用的 up_initialize() 实现,位于 nuttx/arch/arm/common/up_initialize.c 中。然而,此通用 ARM 初始化会调用特定 ARM 芯片提供的功能。对于 STM32 F4,这些功能由 nuttx/arch/arm/src/stm32 中的逻辑提供。

通用 ARM 初始化序列如下:

  1. up_calibratedelay():在 CPU 移植过程中必须执行的一个操作是校准时间延迟循环。如果定义了 CONFIG_ARCH_CALIBRATION,则 up_initialize() 将执行一些特定的延迟循环校准操作。然而,这并不是正常初始化序列的一部分。up_calibratedelay()up_initialize.c 中实现。
  2. up_addregion():基本堆在 os_start() 处理期间设置。但是,如果板支持多个不连续的内存区域,则可以通过此函数将任何额外的内存区域添加到堆中。对于 STM32 F4,up_addregion()nuttx/arch/arm/src/stm32/stm32_allocateheap.c 中实现。
  3. up_irqinitialize():此函数初始化中断子系统。对于 STM32 F4,up_irqinitialize()nuttx/arch/arm/src/stm32_irq.c 中实现。
  4. up_pminitialize():如果定义了 CONFIG_PM,则此函数必须初始化电源管理子系统。此 MCU 特定功能必须在初始化序列的非常早期调用,且在其他设备驱动程序初始化之前(因为它们可能会尝试向电源管理子系统注册)。STM32 平台没有 up_pminitialize() 的实现。
  5. up_dmainitialize():初始化 DMA 子系统。对于 STM32 F4,此 DMA 初始化可以在 nuttx/arch/arm/src/stm32_dma.c 中找到(包括 nuttx/arch/arm/src/stm32f4xxx_dma.c)。
  6. up_timerinit():初始化系统定时器中断。对于 STM32 F4,此函数初始化 ARM Cortex-M SYSTICK 定时器,位于 nuttx/arch/arm/src/stm32_timerisr.c
  7. devnull_register():注册标准 /dev/null
  8. 然后此函数初始化控制台设备(如果有)。这意味着调用以下之一:
    up_serialinit():用于标准串行驱动程序(STM32 F4 的实现位于 nuttx/arch/arm/src/stm32_serial.c)。
    lowconsole_init():用于低级只写串行控制台(实现位于 nuttx/drivers/serial/lowconsole_init.c)。
    ramlog_sysloginit():用于 RAM 控制台(实现位于 nuttx/drivers/ramlog.c)。
  9. up_netinitialize():初始化网络。对于 STM32 F4,此函数位于 nuttx/arch/arm/src/stm32_eth.c
  10. up_usbinitialize():初始化 USB(主机或设备)。对于 STM32 F4,此函数位于 nuttx/arch/arm/src/stm32_otgfsdev.c
  11. up_ledon(LED_IRQSENABLED):最后,up_initialize() 点亮板载特定 LED,指示 IRQ 已启用。

2.3.5 STM32 F4 IDLE 线程

默认的 STM32 F4 IDLE 线程位于 nuttx/arch/arm/src/stm32_idle.c 中。此默认版本的功能非常少:

  1. 它包含一个示例“框架”函数,展示了如果启用了 CONFIG_PM,可以执行的操作(此示例代码在默认 IDLE 逻辑中未完全实现)。
  2. 它执行 Cortex-M Thumb-2 指令 wfi,使 CPU 进入睡眠状态,直到下一个中断发生。

2.4 应用程序初始化

os_start() 的 OS 初始化阶段结束时,用户应用程序通过创建一个新任务在入口点 user_start() 处启动。在基于 NuttX 构建的每个应用程序中,必须有一个名为 user_start() 的唯一入口点。user_start() 函数中执行的任何其他初始化完全取决于应用程序。

2.4.1 简单的“Hello, World!”应用程序

最简单的用户应用程序是“Hello, World!”示例。示例代码位于 apps/examples/hello 中。

以下是完整示例:

int user_start(int argc, char *argv[])
{
    printf("Hello, World!!\n");
    return 0;
}

在这种情况下,不需要额外的应用程序初始化。它只是“打招呼”然后退出。

2.5 在源树之外构建带有板级特定部分的 NuttX

至少有以下四种方法可以在源树之外构建带有板级特定部分的 NuttX:

1. 使用 make export

NuttX 提供了一个名为 make export 的 Make 目标。它会构建 NuttX,然后将所有头文件、库、启动对象和其他构建组件打包到一个 .zip 文件中。你可以将该 .zip 文件移动到任何构建环境中。甚至可以在 DOS CMD 窗口中构建 NuttX。

此 Make 目标在顶层 nuttx/README.txt 文件中有详细说明。

2. 替换 apps/ 目录

你可以替换整个 apps/ 目录。如果 apps/ 目录中没有你需要的内容,可以在 .config 文件中定义 CONFIG_APPS_DIR,使其指向一个不同的自定义应用程序目录。你可以根据需要从旧的 apps/ 目录中复制任何内容到新的自定义应用程序目录。

此方法在以下文档中有详细说明:
NuttX/configs/README.txt
nuttx/Documentation/NuttxPortingGuide.html(在线地址:http://nuttx.sourceforge.net/NuttxPortingGuide.html#apndxconfigs 中的“Build options”部分)
apps/README.txt 文件。

3. 使用 external 子目录

如果你喜欢 apps/ 目录中的随机内容,但希望用你自己的外部子目录扩展现有组件,则有一种简单的方法可以实现:只需在 apps/ 目录下创建一个指向你的应用程序子目录的符号链接 externalapps/Makefile 会自动检查是否存在 apps/external 目录,如果存在,则会自动将其包含到构建中(无需将其添加到 apps/.config 文件中)。

apps/Makefile 的此功能仅在此处记录。

例如,你可以创建一个名为 install.sh 的脚本,用于安装自定义应用程序、配置和板级特定目录。该脚本可能会执行以下操作:
• 将你的 MyBoard 目录复制到 configs/MyBoard
• 在 apps/external 中添加一个指向 MyApplication 的符号链接。
• 通过执行以下命令通常配置 NuttX:

tools/configure.sh MyBoard/MyConfiguration

或者简单地通过复制以下文件:
defconfig -> nuttx.config
setenv.sh -> nuttx/setenv.sh
Make.defs -> nuttx/Make.defs
appconfig -> apps/.config

使用 external 链接特别方便地将“内置”应用程序添加到现有配置中。

4. 自定义链接

你可以向 apps/ 添加任意数量的符号链接目录:
• 在 apps/ 中添加指向其他目录的符号链接。
• 然后在你的 appconfig 文件(即 apps/.config 文件)中添加这些链接的(相对)路径。

这本质上与选项 #3 相同,但不使用特殊的 external 链接,并且允许你添加任意多的链接子目录。顶层的 apps/Makefile 始终会构建 apps/.config 文件中找到的内容(如果存在 external 链接,则也会包含它)。

4. The NuttShell (NSH)

4.1 概述

NuttShell (NSH) 是一个简单的 shell 应用程序,可与 NuttX 一起使用。它在这里有详细描述:http://nuttx.sourceforge.net/NuttShell.html。NSH 支持多种命令,并且(非常)松散地基于 Unix shell 编程中使用的 bash shell 和常见工具。该参考文档提供了 NSH 的良好概述,此处不再赘述。

本节将重点介绍如何自定义 NSH,例如添加新命令、更改初始化序列等。

4.2 NSH 库和 NSH 初始化

NSH 是作为一个库实现的,位于 apps/nshlib 中。作为一个库,它可以被自定义构建到任何遵循以下 NSH 初始化序列的应用程序中。例如,apps/examples/nsh/nsh_main.c 中的代码展示了如何启动 NSH,其逻辑可以集成到自定义代码中。尽管代码只是一个示例,但最终大多数人直接将该示例代码用作应用程序的 main() 函数。

以下是该示例执行的初始化序列:

4.2.1 NSH 初始化序列

NSH 的启动序列非常简单。例如,apps/examples/nsh/nsh_main.c 中的代码展示了如何启动 NSH,它执行以下操作:

  1. 如果有 C++ 静态初始化器,则调用你的 up_cxxinitialize() 实现,该实现会依次调用这些静态初始化器。例如,在 STM3240G-EVAL 板上,up_cxxinitialize() 的实现位于 nuttx/configs/stm3240g-eval/src/up_cxxinitialize.c
  2. 调用 nsh_initialize() 初始化 NSH 库。nsh_initialize() 的详细信息见下文。
  3. 如果启用了 Telnet 控制台,则调用 nsh_telnetstart(),该函数位于 NSH 库中。nsh_telnetstart() 会启动 Telnet 守护进程,监听 Telnet 连接并启动远程 NSH 会话。
  4. 如果启用了本地控制台(可能在串口上),则调用 nsh_consolemain()nsh_consolemain() 也位于 NSH 库中,并且不会返回,从而完成整个 NSH 初始化序列。

4.2.2 nsh_initialize()

NSH 初始化函数 nsh_initialize() 位于 apps/nshlib/nsh_init.c 中。它只做三件事:

  1. nsh_romfsetc()
    如果配置了,它会执行一个 NSH 启动脚本,该脚本可以在目标文件系统的 /etc/init.d/rcS 中找到。nsh_romfsetc() 函数位于 apps/nshlib/nsh_romfsetc.c 中。该函数会:
    • 注册一个 ROMFS 文件系统。
    • 挂载 ROMFS 文件系统。/etcnsh_romfsetc() 默认挂载只读 ROMFS 文件系统的位置。

    ROMFS 映像本身直接构建到固件中。默认情况下,rcS 启动脚本包含以下逻辑:

    # 创建一个 RAMDISK 并将其挂载到 XXXRDMOUNTPOUNTXXX
    mkrd -m XXXMKRDMINORXXX -s XXMKRDSECTORSIZEXXX XXMKRDBLOCKSXXX
    mkfatfs /dev/ramXXXMKRDMINORXXX
    mount -t vfat /dev/ramXXXMKRDMINORXXX XXXRDMOUNTPOUNTXXX
    

    其中,XXXX*XXXX 字符串在创建 ROMFS 映像时会被替换为实际值:
    XXXMKRDMINORXXX:RAM 设备的次设备号,默认值为 1
    XXMKRDSECTORSIZEXXX:RAM 设备的扇区大小。
    XXMKRDBLOCKSXXX:设备中的扇区数。
    XXXRDMOUNTPOUNTXXX:挂载点,默认值为 /tmp

    替换后的值会生成一个类似以下的 rcS 文件:

    # 创建一个 RAMDISK 并将其挂载到 /tmp
    mkrd -m 1 -s 512 1024
    mkfatfs /dev/ram1
    mount -t vfat /dev/ram1 /tmp
    

    该脚本会:
    • 在 /dev/ram1 上创建一个大小为 512*1024 字节的 RAMDISK。
    • 在 /dev/ram1 上格式化一个 FAT 文件系统。
    • 将 FAT 文件系统挂载到配置的挂载点 /tmp

    rcS 模板文件位于 apps/nshlib/rcS.template 中。生成的 ROMFS 文件系统可以在 apps/nshlib/nsh_romfsimg.h 中找到。

  2. nsh_archinitialize()
    接下来执行任何特定于架构的 NSH 初始化(如果有的话)。例如,在 STM3240G-EVAL 板上,此特定于架构的初始化位于 configs/stm3240g-eval/src/up_nsh.c 中。它执行以下操作:
    • 初始化 SPI 设备。
    • 初始化 SDIO。
    • 挂载可能插入的 SD 卡。

  3. nsh_netinit()
    nsh_netinit() 函数位于 apps/nshlib/nsh_netinit.c 中。

4.4 NSH 命令

4.4.1 NSH 命令概述

NSH 支持多种命令,这些命令列在 NSH 文档中:[http://nuttx.sourceforge.net/NuttShell.html #cmdoverview](http://nuttx.sourceforge.net/NuttShell.html #cmdoverview)。然而,并非所有命令都始终可用,因为许多命令依赖于特定的 NuttX 配置选项。你可以在 NSH 提示符下输入 help 命令查看实际可用的命令:

nsh> help

例如,如果禁用了网络支持,则所有与网络相关的命令都会从 nsh> help 显示的命令列表中消失。你可以在以下文件中找到特定命令的依赖关系:
http://nuttx.sourceforge.net/NuttShell.html#cmdddependencies

4.4.2 添加新的 NSH 命令

向 NSH 添加新命令非常简单。你需要完成以下两件事:

  1. 实现你的命令
    例如,如果你想向 NSH 添加一个名为 mycmd 的新命令,首先需要用以下原型实现 mycmd 的代码:

    int cmd_mycmd(FAR struct nsh_vtbl_s *vtbl, int argc, char **argv);
    

    argcargv 用于将命令行参数传递给 NSH 命令。
    vtbl 是一个指向会话特定状态信息的指针,主要用于输出数据到控制台。在 NSH 命令中不能使用 printf(),而是使用:

    void nsh_output(FAR struct nsh_vtbl_s *vtbl, const char *fmt, …);
    

    • 如果你只想在控制台输出“Hello, World!”,则整个命令实现可能如下:

    int cmd_mycmd(FAR struct nsh_vtbl_s *vtbl, int argc, char **argv)
    {
        nsh_output(vtbl, "Hello, World!");
        return 0;
    }
    

    • 新命令的原型应放在 apps/examples/nshlib/nsh.h 中。

  2. 将命令添加到 NSH 命令表
    NSH 支持的所有命令都出现在一个名为 g_cmdmap[] 的表中,该表位于 apps/examples/nshlib/nsh_parse.c 中。cmdmap_s 结构定义如下:

    struct cmdmap_s
    {
        const char *cmd;      /* 命令名称 */
        cmd_t handler;        /* 处理命令的函数 */
        uint8_t minargs;      /* 最小参数数量(包括命令本身) */
        uint8_t maxargs;      /* 最大参数数量(包括命令本身) */
        const char *usage;    /* 'help' 命令的用法说明 */
    };
    

    你需要为你的命令添加一个条目到 g_cmdmap[] 表中。例如:

    { "mycmd", cmd_mycmd, 1, 1, NULL },
    

4.5 NSH “内置”应用程序

4.5.1 概述

除了 NSH 的命令外,外部程序也可以作为 NSH 命令执行。这些外部程序被称为“内置”应用程序。尽管术语有些令人困惑,但这些应用程序实际上是 NuttX 外部的。

当配置选项 CONFIG_NSH_BUILTIN_APPS 启用时,这些内置应用程序可以通过在 NSH 提示符下输入其名称来执行。它们会出现在“Builtin Apps”部分。

4.5.2 命名应用程序

概述

支持 NSH 内置应用程序的底层逻辑称为“命名应用程序”。命名应用程序逻辑位于 apps/namedapp 中,主要完成以下任务:

  1. 支持注册机制,使命名应用程序可以在构建时动态注册。
  2. 提供查找、列出和执行命名应用程序的实用函数。

命名应用程序实用函数

这些实用函数的原型位于 apps/include/apps.h 中,包括:
int namedapp_isavail(FAR const char *appname):检查构建时注册的应用程序是否可用。
const char *namedapp_getname(int index):返回由索引指向的内置应用程序的名称。
int exec_namedapp(FAR const char *appname, FAR const char **argv):执行构建时注册的内置应用程序。

自动生成的头文件

当 NuttX 第一次构建时,应用程序入口点及其需求会收集到以下两个文件中:

  1. apps/namedapp/namedapp_proto.h:应用程序任务入口点的原型。
  2. apps/namedapp/namedapp_list.h:应用程序的特定信息和启动需求。

命名应用程序的注册

命名应用程序信息在 make 上下文构建阶段收集。例如,apps/examples/hello 中的应用程序可以通过以下方式注册:

  1. apps/examples/hello/main.c 中定义主函数:
    int hello_main(int argc, char *argv[])
    {
        printf("Hello, World!!\n");
        return 0;
    }
    
  2. apps/examples/hello/Makefile 中注册应用程序:
    ifeq ($(CONFIG_EXAMPLES_HELLO_BUILTIN),y)
    $(call REGISTER,hello,SCHED_PRIORITY_DEFAULT,2048,hello_main)
    @touch $@
    endif
    

4.5.3 同步内置应用程序

默认情况下,从 NSH 命令行启动的内置命令会异步运行。如果希望 NSH 等待命令执行完成,可以启用以下配置选项:

CONFIG_SCHED_WAITPID=y

4.5.4 应用程序配置文件

应用程序的构建通过 appconfig 文件配置,该文件位于 configs/<board>/<configuration>/appconfig 中。其内容必须定义要添加到构建中的应用程序,例如:

CONFIGURED_APPS += examples/hello

变更计划

未来将引入一种自动配置系统来取代 appconfig 文件,从而实现更高效的 NuttX 配置。有关详细信息,请参阅:http://tech.groups.yahoo.com/group/nuttx/message/1604

4.6 自定义 NSH 初始化

4.6.1 自定义 NSH 启动行为的方法

有三种方法可以自定义 NSH 的启动行为,按难度递增的顺序列出如下:

  1. 扩展 configs/stm3240g-eval/src/up_nsh.c 中的初始化逻辑
    该文件中的逻辑会在每次启动 NSH 时被调用,特别适合用于与设备相关的初始化操作。

  2. 替换 apps/examples/nsh/nsh_main.c 中的示例代码
    NSH 是一个位于 apps/nshlib 的库,而 apps/examples/nsh 只是一个简单的启动函数(user_start()),它会立即运行并启动 NSH。如果你希望在其他内容之前运行自定义逻辑,可以编写自己的 user_start() 函数,并从中启动其他任务。

  3. 使用 NSH 启动脚本
    NSH 支持在首次运行时执行启动脚本。这种机制的优点是启动脚本可以包含任何 NSH 命令,因此可以用很少的代码完成大量工作。缺点是创建启动脚本的过程相对复杂,值得单独详细说明。

4.6.2 NuttShell 启动脚本

NSH 启动脚本概述

NSH 支持通过配置选项提供启动脚本的功能。启动脚本可以包含任何 NSH 支持的命令(即输入 nsh> help 时看到的命令)。通常情况下,此功能通过 CONFIG_NSH_ROMFSETC=y 启用,但它还依赖于其他几个相关的配置选项,具体描述见 NSH 特定配置设置文档:[NuttShell Configuration](file:///c:/cygwin/home/patacongo/projects/nuttx/nuttx/trunk/nuttx/Documentation/NuttShell.html#nshconfiguration)。

启动脚本功能还依赖于以下条件:
CONFIG_DISABLE_MOUNTPOINT=n:如果禁用了挂载点支持,则无法挂载任何文件系统。
CONFIG_NFILE_DESCRIPTORS >= 4:必须有足够的文件描述符才能使用文件系统。
CONFIG_FS_ROMFS=y:启用 ROMFS 文件系统支持。

默认启动行为

当所有配置选项设置为默认值时,启用 CONFIG_NSH_ROMFSETC 后,NSH 在启动时的行为如下:

  1. 创建只读 RAM 磁盘(ROM 磁盘)
    NSH 会创建一个包含小型 ROMFS 文件系统的只读 RAM 磁盘,文件系统结构如下:

    -- init.d/
       -- rcS
    

    其中,rcS 是 NSH 的启动脚本。

  2. 挂载 ROMFS 文件系统
    NSH 会将 ROMFS 文件系统挂载到 /etc,结果如下:

    -- dev/
       -- ram0
    -- etc/
       -- init.d/
          -- rcS
    
  3. 默认 rcS 脚本内容
    默认情况下,rcS 脚本的内容如下:

    # 创建一个 RAMDISK 并将其挂载到 /tmp
    mkrd -m 1 -s 512 1024
    mkfatfs /dev/ram1
    mount -t vfat /dev/ram1 /tmp
    
  4. 执行启动脚本
    NSH 会在启动时(在第一个 NSH 提示符之前)执行 /etc/init.d/rcS 脚本。脚本执行后,根文件系统的结构如下:

    -- dev/
       -- ram0
       -- ram1
    -- etc/
       -- init.d/
          -- rcS
    -- tmp/
    

示例配置

以下是一些在 NuttX 配置文件中将 CONFIG_NSH_ROMFSETC=y 的示例配置:
configs/hymini-stm32v/nsh2
configs/ntosd-dm320/nsh
configs/sim/nsh
configs/sim/nsh2
configs/sim/nx
configs/sim/nx11
configs/sim/touchscreen
configs/vsn/nsh

在大多数情况下,这些配置会设置默认的 /etc/init.d/rcS 脚本。默认脚本位于 apps/nshlib/rcS.template 中(模板中的占位符值如 XXXMKRDMINORXXX 会在构建时通过 sed 替换)。此默认配置会创建一个 RAM 磁盘并将其挂载到 /tmp,如上所述。

如果默认行为不符合需求,可以通过在配置文件中定义 CONFIG_NSH_ARCHROMFS=y 来提供自定义的 rcS 脚本。NuttX 源代码树中唯一使用自定义 /etc/init.d/rcS 文件的示例是 configs/vsn/nsh,其 defconfig 文件中包含以下定义:

CONFIG_NSH_ARCHROMFS=y  # 支持特定架构的 ROMFS 文件

修改 ROMFS 映像

/etc 目录的内容保留在以下文件中:
• 如果未定义 CONFIG_NSH_ARCHROMFS,则存储在 apps/nshlib/nsh_romfsimg.h 中。
• 如果定义了 CONFIG_NSH_ARCHROMFS,则存储在 include/arch/board/rcS.template 中。

要修改启动行为,需要研究以下内容:

1. 配置选项

额外的 CONFIG_NSH_ROMFSETC 配置选项可以在 NSH 特定配置文档中找到:[NuttShell Configuration](file:///c:/cygwin/home/patacongo/projects/nuttx/nuttx/trunk/nuttx/Documentation/NuttShell.html#nshconfiguration)。

2. tools/mkromfsimg.sh 脚本

tools/mkromfsimg.sh 脚本用于创建 nsh_romfsimg.h 文件。它不会自动执行。如果需要更改与创建和挂载 /tmp 目录相关的配置设置,则必须使用此脚本重新生成头文件。

该脚本的行为依赖于以下内容:
配置设置:当前安装的配置。
genromfs 工具:用于生成 ROMFS 文件系统映像,可从 genromfs 官方网站 获取,或包含在 NuttX Buildroot 工具链中(快照文件:misc/tools/genromfs-0.5.2.tar.gz)。
xxd 工具:用于生成 C 头文件,是完整 Linux 或 Cygwin 安装的一部分。
rcS.template 文件:位于 apps/nshlib/rcS.template 中,或者如果定义了 CONFIG_NSH_ARCHROMFS,则使用 include/arch/board/rcS.template

3. rcS.template 文件

apps/nshlib/rcS.template 文件包含 rcS 文件的通用形式;配置值会被插入到该模板文件中以生成最终的 rcS 文件。

如果未定义 CONFIG_NSH_ARCHROMFS,则使用默认的 apps/nshlib/rcS.template 生成标准的 apps/nshlib/nsh_romfsimg.h 文件。

如果定义了 CONFIG_NSH_ARCHROMFS,则使用位于 configs/<board>/include 中的自定义板级特定 nsh_romfsimg.h 文件。注意,当 OS 配置时,include/arch/board 会被链接到 configs/<board>/include

NuttX 源代码树中唯一使用自定义 /etc/init.d/rcS 文件的示例是 configs/vsn/nsh,其自定义脚本位于:

configs/vsn/include/rcS.template

所有启动行为都包含在 rcS.template 中。mkromfsimg.sh 脚本的作用是:

  1. 将特定配置设置应用到 rcS.template,以创建最终的 rcS 文件。
  2. 生成包含 ROMFS 文件系统映像的头文件 nsh_romfsimg.h

要完成此操作,mkromfsimg.sh 脚本需要使用两个工具:
genromfs 工具:用于生成 ROMFS 文件系统映像。
xxd 工具:用于创建 C 头文件。

生成的 ROMFS 文件系统示例(configs/vsn 情况)可以在以下位置找到:

configs/vsn/include/rcS.template
posted @ 2025-04-07 14:07  容景云  阅读(870)  评论(0)    收藏  举报