第2章:NuttX源码结构解析

好,咱们开始啃源码了。说实话,我第一次接触NuttX的目录结构时,也有点懵。跟Linux那种庞然大物比,NuttX算轻量级了,但麻雀虽小五脏俱全。这一章,我带你把它的骨架摸清楚。

2.1 源码目录树概览

先看顶层目录。你下载完NuttX源码后,会看到这么几个关键目录:

nuttx/
├── arch/          # 架构相关代码(重点)
├── boards/        # 板级支持包
├── drivers/       # 设备驱动框架
├── fs/            # 文件系统
├── include/       # 头文件
├── libs/          # 库函数
├── mm/            # 内存管理
├── net/           # 网络协议栈
├── sched/         # 任务调度
├── tools/         # 构建工具
└── Kconfig        # 顶层配置入口

我个人习惯,拿到一个新RTOS先看arch/boards/。为什么?因为你要移植驱动,这两块是直接打交道的。其他目录像sched/mm/,说实话,做驱动开发时很少动它们。

核心思路:驱动开发关注的是「硬件怎么跟系统对接」,而不是系统内部怎么调度任务。所以你的精力应该集中在arch和boards上。

2.2 arch/arm/src/stm32目录深度分析

这个目录,是我们移植串口驱动的核心战场。我带你一层层剥开:

arch/arm/src/stm32/
├── stm32_serial.c      # 串口驱动主文件(我们的目标)
├── stm32_serial.h      # 串口驱动头文件
├── stm32_lowputc.c     # 底层字符输出(调试用)
├── stm32_uart.h        # UART寄存器定义
├── stm32_gpio.c        # GPIO配置
├── stm32_gpio.h        # GPIO头文件
├── stm32_rcc.c         # 时钟配置
├── stm32_rcc.h
├── stm32_dma.c         # DMA支持
├── stm32_dma.h
├── stm32_irq.c         # 中断控制器
├── stm32_irq.h
├── Make.defs           # 编译规则
└── Kconfig             # 配置选项

你看,串口驱动不是孤立的。它依赖GPIO、时钟、中断、甚至DMA。我在项目中遇到过一个问题:串口能发送但收不到数据,查了两天,最后发现是GPIO的复用功能没配对。嗯,这种坑踩过一次就记住了。

2.2.1 stm32_serial.c 内部结构

打开这个文件,你会看到典型的NuttX驱动框架:

// 驱动入口结构
struct uart_ops_s g_stm32_uart_ops = {
    .setup      = stm32_setup,
    .shutdown   = stm32_shutdown,
    .attach     = stm32_attach,
    .detach     = stm32_detach,
    .ioctl      = stm32_ioctl,
    .receive    = stm32_receive,
    .send       = stm32_send,
    .txempty    = stm32_txempty,
};

// 每个UART实例
struct stm32_uart_s {
    uint32_t     base;       // 寄存器基地址
    uint32_t     baud;       // 波特率
    uint8_t      irq;        // 中断号
    uint8_t      parity;     // 校验位
    uint8_t      bits;       // 数据位
    uint8_t      stopbits2;  // 停止位
    // ... 还有更多
};

这里有个关键点:uart_ops_s是NuttX定义的通用串口操作接口。你只要实现这些函数,系统就能自动把串口挂接到文件系统上。说白了,你写驱动就是在填这个结构体。

我的经验:移植时不要从头写,直接复制stm32_serial.c,然后改寄存器操作部分。STM32系列之间寄存器差异不大,改起来很快。

2.3 Kconfig与Makefile体系

这部分是NuttX的构建灵魂。很多新手觉得Kconfig麻烦,其实它就是个「开关面板」。

2.3.1 Kconfig配置体系

arch/arm/src/stm32/Kconfig里,你会看到:

config STM32_SERIAL
    bool "STM32 Serial Driver"
    default y
    depends on STM32
    select UART
    ---help---
        启用STM32串口驱动支持

config STM32_USART1
    bool "USART1"
    default y
    depends on STM32_SERIAL
    ---help---
        启用USART1外设

为什么要有这个?我举个例子。有一次客户要求裁剪系统,把不需要的外设全关掉。如果没有Kconfig,你得手动删代码。有了它,在make menuconfig里勾勾选选就行了。

注意:Kconfig里的depends onselect很容易搞混。前者是「依赖」,后者是「自动选中」。我见过有人把select写反了,导致编译出一堆没用的驱动。

2.3.2 Makefile编译规则

arch/arm/src/stm32/Make.defs

# 串口驱动
ifeq ($(CONFIG_STM32_SERIAL),y)
CHIP_CSRCS += stm32_serial.c
endif

# DMA支持
ifeq ($(CONFIG_STM32_DMA),y)
CHIP_CSRCS += stm32_dma.c
endif

这套机制很巧妙。你配置了CONFIG_STM32_SERIAL=y,编译器才会把stm32_serial.c加进来。否则,这个文件根本不会参与编译。这样既节省空间,又避免编译错误。

2.4 board目录结构

最后看boards目录。这是板级相关的代码,每个开发板一个文件夹:

boards/arm/stm32/
├── stm32f103_minimum/    # 最小系统板
│   ├── src/
│   │   ├── stm32_boot.c      # 启动初始化
│   │   ├── stm32_leds.c      # LED控制
│   │   └── stm32_serial.c    # 板级串口配置
│   ├── include/
│   │   └── board.h           # 板级头文件
│   ├── Kconfig               # 板级配置
│   └── Make.defs             # 板级编译规则
├── nucleo-f103rb/        # Nucleo开发板
└── stm32f4discovery/     # 探索者开发板

board目录里最重要的就是board.h。它定义了引脚映射:

/* USART1引脚映射 */
#define GPIO_USART1_TX    GPIO_USART1_TX_1  /* PA9 */
#define GPIO_USART1_RX    GPIO_USART1_RX_1  /* PA10 */

/* USART2引脚映射 */
#define GPIO_USART2_TX    GPIO_USART2_TX_2  /* PD5 */
#define GPIO_USART2_RX    GPIO_USART2_RX_2  /* PD6 */

这里有个坑:同一个USART可能有多个引脚映射选项。比如USART1的TX可以是PA9,也可以是PB6。你得看原理图,确认板子上实际用了哪个。我曾经因为想当然用了默认引脚,结果焊好板子发现串口没反应——嗯,查了半天才发现是引脚映射错了。

2.5 本章知识体系图

下面这张图,帮你把整个源码结构串起来:

NuttX源码结构(串口驱动视角) nuttx/ 根目录 arch/arm/src/stm32/ boards/arm/stm32/ arch层关键文件 • stm32_serial.c — 串口驱动主文件 • stm32_lowputc.c — 底层字符输出 • stm32_gpio.c — GPIO配置 • Kconfig / Make.defs — 构建配置 board层关键文件 • board.h — 引脚映射定义 • stm32_boot.c — 板级初始化 • stm32_serial.c — 板级串口配置 • Kconfig — 板级配置选项 uart_register() → 挂载到 /dev/ttySx open() / read() / write() / ioctl()

这张图展示了从源码根目录到最终用户接口的完整链路。你写驱动时,主要工作在arch层和board层。arch层负责芯片级的寄存器操作,board层负责板级的引脚配置。两层配合好了,驱动就活了。

总结一下:

  • arch层:芯片通用代码,跟具体板子无关。比如STM32F103和STM32F407的串口寄存器操作基本一样,代码可以复用。
  • board层:板级特有配置,比如引脚映射、时钟频率。换板子就改这里。
  • Kconfig/Makefile:构建系统的开关和规则。配好了,编译自动搞定。

我的建议:刚开始移植时,先找一个跟你板子最接近的现有board目录,复制一份改。比如你用STM32F103C8T6,就复制stm32f103_minimum,然后改引脚和时钟。这样效率最高。

好了,源码结构就讲到这里。下一章我们会动手搭建开发环境,然后开始真正的串口驱动移植。嗯,做好准备,代码要来了。


专注资料整理