第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 on和select很容易搞混。前者是「依赖」,后者是「自动选中」。我见过有人把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 本章知识体系图
下面这张图,帮你把整个源码结构串起来:
这张图展示了从源码根目录到最终用户接口的完整链路。你写驱动时,主要工作在arch层和board层。arch层负责芯片级的寄存器操作,board层负责板级的引脚配置。两层配合好了,驱动就活了。
总结一下:
- arch层:芯片通用代码,跟具体板子无关。比如STM32F103和STM32F407的串口寄存器操作基本一样,代码可以复用。
- board层:板级特有配置,比如引脚映射、时钟频率。换板子就改这里。
- Kconfig/Makefile:构建系统的开关和规则。配好了,编译自动搞定。
我的建议:刚开始移植时,先找一个跟你板子最接近的现有board目录,复制一份改。比如你用STM32F103C8T6,就复制stm32f103_minimum,然后改引脚和时钟。这样效率最高。
好了,源码结构就讲到这里。下一章我们会动手搭建开发环境,然后开始真正的串口驱动移植。嗯,做好准备,代码要来了。