第4章 BSP目录结构分析:QNX BSP源码目录布局与关键文件说明

做BSP移植,第一件事是什么?

不是急着改代码,而是先把目录结构摸清楚。我见过不少新手,上来就翻buildfile,结果连文件在哪都找不到。说白了,QNX的BSP目录布局是有套路的。搞懂了这套路,你才能知道去哪改、改什么。

4.1 标准BSP目录布局

QNX官方提供的BSP,目录结构大致是这样的:

board_name/
├── build/
│   ├── common.build
│   ├── board.build
│   └── buildfile
├── images/
├── src/
│   ├── hw/
│   │   ├── startup/
│   │   ├── init/
│   │   └── lib/
│   └── ...
├── patches/
├── doc/
└── Makefile

嗯,这里要注意,board_name就是你目标板的名字。比如我做过的一个i.MX6Q的板子,就叫imx6q_sabrelite

每个目录的职责很清晰:

目录/文件 作用
build/ 构建脚本的核心,放buildfile和构建配置
images/ 编译生成的镜像文件,比如IFS、OS图像
src/hw/startup/ 启动代码,板级初始化就在这里
src/hw/init/ 系统初始化脚本,比如io-pkt、devb-*的启动参数
patches/ 针对QNX官方代码的补丁
doc/ 文档,有时候会有readme.txt

我个人习惯,拿到一个新BSP,先看build/目录。因为这里决定了整个系统的构建方式。

4.2 核心文件:buildfile

buildfile是整个BSP的灵魂。它告诉mkifs工具:你要把哪些东西打包进镜像,怎么配置系统。

一个典型的buildfile长这样:

[virtual=armle-v7,raw] .bootstrap = {
    startup-imx6q-sabrelite
    path=/proc/boot
}

[+script] .script = {
    # 加载驱动
    devb-sdmmc-imx6 &
    waitfor /dev/sdmmc0

    # 启动网络
    io-pkt-v6-hc -d imx6 &
    waitfor /dev/io-pkt

    # 启动文件系统
    mount -t qnx6 /dev/sdmmc0t177 /fs/sdmmc0

    # 启动shell
    [+session] ksh
}

# 包含的文件
libc.so.3
libcam.so.2
...

这里有几个关键点:

  • [virtual=armle-v7,raw]:指定虚拟地址和格式。armle-v7是ARM小端模式,raw表示原始二进制。
  • .bootstrap:启动加载器,就是startup-*那个文件。它负责最底层的硬件初始化。
  • [+script] .script:启动脚本。系统启动后会按顺序执行这里面的命令。
  • [+session]:表示启动一个会话,比如shell。

我曾经踩过一个坑:在.script里忘了加waitfor,结果驱动还没加载完,后面的命令就开始执行了。系统启动到一半就卡死。嗯,从那以后我每条命令后面都老老实实加上waitfor

注意:buildfile里的路径是相对于镜像根目录的。比如/proc/boot是启动分区,/dev/sdmmc0是设备节点。别搞混了。

4.3 构建配置:common.build

common.build是共享的构建配置。它定义了一些通用的变量和规则,可以被多个board.build引用。

举个例子:

# common.build
COMMON_CFLAGS = -O2 -Wall -Werror
COMMON_LDFLAGS = -L$(QNX_TARGET)/$(CPUVARDIR)/lib

# 通用驱动列表
DRIVERS = devb-sdmmc io-pkt-v6-hc devc-ser8250

# 通用库
LIBS = libc.so.3 libcam.so.2 libsocket.so.2

为什么要搞个common.build?

你想想看,如果你有多个板子,比如imx6q和imx6dl,它们的很多配置是相同的。把公共部分抽出来,维护起来就轻松多了。

我建议你在common.build里放这些东西:

  • 编译器标志(CFLAGS、LDFLAGS)
  • 通用的驱动列表
  • 通用的库文件
  • 通用的启动参数

4.4 板级配置:board.build

board.build是针对特定板子的配置。它通常会include common.build,然后覆盖或添加板级特有的内容。

# board.build
include common.build

# 板级特有配置
BOARD_CFLAGS = -DBOARD_IMX6Q_SABRELITE -DCPU_CORTEX_A9

# 板级驱动
BOARD_DRIVERS = devb-sdmmc-imx6 io-pkt-v6-hc-imx6

# 板级启动参数
BOARD_STARTUP_ARGS = -v -D imx6q_sabrelite

# 内存布局
MEMORY = {
    ram = 0x80000000, 0x40000000  # 1GB DDR
    flash = 0x60000000, 0x08000000  # 128MB NOR
}

这里有个细节:BOARD_CFLAGS里定义了BOARD_IMX6Q_SABRELITE这个宏。在startup代码里,你可以用这个宏来做条件编译:

#ifdef BOARD_IMX6Q_SABRELITE
    // 初始化i.MX6Q的特定外设
    imx6q_init_uart();
    imx6q_init_sdram();
#endif

这样做的好处是,一套startup代码可以支持多个板子,只要用宏来区分就行。

小技巧:board.build里尽量只放板级特有的东西。通用的部分交给common.build。这样当你要支持新板子时,只需要新建一个board.build,然后include common.build,再改几行配置就行了。

4.5 三者的协作关系

说白了,这三个文件的关系是这样的:

  • buildfile:最终的执行者,它引用common.build和board.build里的变量。
  • common.build:提供通用配置,减少重复。
  • board.build:提供板级特有配置,实现差异化。

在buildfile里,你通常会这样引用:

# buildfile
include common.build
include board.build

# 使用变量
[+script] .script = {
    $(BOARD_DRIVERS)
    ...
}

这样,当你换板子时,只需要换board.build,buildfile和common.build基本不用动。

我记得有一次,客户要求在一周内支持三块不同的板子。我就是靠这种分层设计,只改了board.build和startup代码,buildfile几乎没动。最后三天就搞定了。

4.6 避坑指南

我曾经犯过的错:

  • 在common.build里定义了DRIVERS,又在board.build里重新定义了DRIVERS,结果common.build的被覆盖了。后来我改用+=来追加。
  • buildfile里忘了include common.build,结果编译时一堆未定义的变量。嗯,这种低级错误我犯过一次就记住了。
  • board.build里的内存布局写错了,导致系统启动时内存访问异常。建议每次改内存参数后,先用mkifs -v检查一下。

4.7 小结

这一章我们聊了BSP的目录结构和三个核心文件。你只要记住:

  • buildfile是总指挥,决定系统怎么构建
  • common.build是公共库,放通用配置
  • board.build是板级配置,放差异化内容

搞懂了这些,你就能在BSP的海洋里游刃有余了。下一章,我们深入startup代码,看看系统是怎么从零开始启动的。