简介:这是一份面向嵌入式系统开发者的VxWorks板级支持包(BSP)参考工程,针对ARM9架构的S3C2410X处理器,核心解决将VxWorks实时操作系统移植到该平台时涉及的内存布局、中断控制器、串口通信、定时器、GPIO以及网络接口等硬件适配问题。资源共37个文件,以C源码、头文件、汇编启动代码、目标文件、配置文件与Makefile构建脚本为主,压缩包约1000KB;内容覆盖从ROM启动、硬件初始化和驱动挂接的完整链路,并附有阅读说明文本。其中可见cs8900a以太网驱动、串口驱动及中断控制相关实现,既可直接阅读学习,也可作为定制开发基线。已有188人学习/下载,适合正在基于S3C2410X开展项目或需要对照VxWorks BSP内部结构的系统工程师。通过该工程可梳理从硬件上电、BSP初始化到内核启动的完整流程,同时参考驱动代码编写、寄存器配置和交叉编译调试的常见做法,结合说明文件可降低上手门槛,是构建稳定嵌入式平台的有效资料。
1. 为什么 ARM9 板卡非要一个像样的 VxWorks BSP:先讲清它是什么,再谈值不值得折腾
拿到一块 ARM9 板卡,第一件事就是把 VxWorks 镜像烧进去,结果串口工具里一个设备都没枚举出来,甚至上电后整个系统直接黑屏。这种场景大概率不是镜像损坏,而是 BSP 没搭对。VxWorks 的 BSP 是操作系统与硬件之间的适配层,负责从 CPU 上电后的第一条指令开始,把内存、串口、中断、网卡逐项初始化好,再把控制权交给 VxWorks 内核。没有它,再完整的镜像也只是一坨不能启动的黑匣子。这篇笔记按一套可运行的 ARM9 BSP 作为线索,拆文件、改配置、编译、跑驱动,最后给一份踩坑清单,适合正从裸机转 VxWorks、或者在公司已有 BSP 上做二次开发的从业者。
2. BSP 内部结构:ARM9 上 VxWorks 启动路径与必改文件
2.1 从 bootrom 到 VxWorks 镜像的两次搬运
ARM9 上电后没有 BIOS 帮你初始化内存,也没有固件帮你搬代码,所有脏活累活都得从复位向量开始自己干。VxWorks 在 ARM9 上最典型的启动方式分两段:第一段是 bootrom,它以裸机程序形态放在 Flash 或 CS0 映射的 ROM 空间里;第二段才是真正的 VxWorks 内核镜像。
常见做法是把 bootrom 编译成一个所谓的“自解压、自定位”程序。它的汇编入口一般叫 romInit.s,核心做四件事:关中断、配置 CPU 模式、初始化 SDRAM 控制器、设置好临时栈。等内存控制器可用之后,再把 bootrom 自身或 VxWorks 镜像搬运到 RAM 中运行。因为 ARM9 硬件上天生没有“直接在 Flash 里跑 C 代码”的便利条件,所以这段汇编代码必须仔细对待。
我一般会把 romInit.s 分成两个阶段来看:phase A 是复位后的几条汇编指令,只负责最小的硬件初始化;phase B 才允许调用 C 函数。这样做的原因是很多 ARM9 芯片内存控制器没有配置好之前,压栈都要出错,更别谈调用 C 语言。VxWorks 官方模板也遵循这个节奏,只是不同芯片的寄存器地址和时序参数不同罢了。
第二阶段由 bootrom 里的 C 代码启动,把解压后的 vxWorks 镜像搬到最终内存地址,然后跳转到内核入口。很多人在这一步翻车,是因为搞混了“bootrom 运行期地址”和“VxWorks 镜像链接地址”:bootrom 里搬运用的源地址是 Flash 地址,目标地址是 SDRAM 地址,而 VxWorks 镜像编译时用的链接地址又必须和 config.h 里的 LOCAL_MEM_LOCAL_ADRS 完全一致。三者里任何一个对不上,系统就跑飞。
2.2 BSP 目录里那些“不能删”的文件
VxWorks BSP 的目录结构看起来每个芯片都不一样,但抽出共性之后,下面这几个文件几乎是所有 ARM9 BSP 都绕不开的。下面这张表可以当检索清单用。
| 文件名 | 职责 | 移植时重点改什么 |
|---|---|---|
| config.h | 系统级配置:内存大小、时钟频率、串口地址 | 内存基址、CPU 时钟、外设寄存器地址 |
| bspname.h | 芯片寄存器地址和中断号定义 | 根据硬件原理图逐一核对 |
| romInit.s | 复位汇编入口 | 内存控制器初始化、栈指针 |
| sysLib.c | 板级初始化函数,包含 sysHwInit | 外设电源、总线时序、中断控制器 |
| sysSerial.c | 串口驱动 | 波特率时钟源、UART 基址 |
| sysNet.c | 网卡初始化与 END 驱动注册 | PHY 地址、MAC 地址来源、中断号 |
| Makefile | 编译规则 | CPU 类型、工具链、内存布局 |
要特别强调 bspname.h 不是单纯“抄模板”就行的,它里面每个宏都对应芯片手册上的一页。比如串口的收发寄存器偏址是多少、中断控制器是 VIC 还是 GPIO 模拟,这些决定了你后续驱动能不能起来。很多老工程师习惯先把 bspname.h 和原理图对一遍再动 config.h,这个习惯能省掉后面无数调试时间。
3. 移植实践:从现有 BSP 改出 ARM9 模板的 config.h、Makefile 和链接脚本
3.1 先把 config.h 调到能跑:内存映射、时钟源和串口参数
拿到一套可用模板之后,不要急着看驱动,先把 config.h 里的内存和时钟配置改成目标 ARM9 芯片的真实参数。最常见写法的关键宏如下:
/* 本地内存映射 */ #define LOCAL_MEM_LOCAL_ADRS 0x00100000 /* SDRAM 重映射地址 */ #define LOCAL_MEM_SIZE 0x02000000 /* 32MB */ #define USER_RESERVED_MEM 0x00010000 /* 为启动代码预留 */ #define SDRAM_BASE_ADRS 0x20000000 /* 物理 SDRAM 基址 */ #define FLASH_BASE_ADRS 0x00000000 /* Flash 基址 */ /* 时钟 */ #define CPU_CLK_RATE 200000000 /* 200MHz 主频 */ #define AUX_CLK_RATE 32000000 /* 外设时钟 */ /* 串口参数 */ #define UART_BASE_ADRS 0x90000000 /* UART0 寄存器基址 */ #define UART_CLK_RATE 32000000 #define CONSOLE_BAUD_RATE 115200这段代码的每个宏都直接参与编译。LOCAL_MEM_LOCAL_ADRS 是 VxWorks 内核加载后的运行地址,我也吃过亏:原来模板里是 0x00100000,但目标板子 SDRAM 实际挂在 0x20000000,明明物理上是同一片内存,地址写错后系统要么启动即死,要么内存校验失败。之所以拆出 SDRAM_BASE_ADRS,是因为 bootrom 早期需要按物理地址访问内存控制器,而内核链接地址可以重映射在同一块区域的另一个地址上。
UART_CLK_RATE 也是重点,不要想当然写成 CPU 主频。很多 ARM9 外设时钟来自 PLL 分频,串口波特率计算用的是这个分频后的时钟。如果 UART_CLK_RATE 填错,现象是串口有输出但全是乱码。我一般会先烧一个最小的裸机串口程序验证波特率,再回过来改 config.h。
3.2 Makefile 与链接脚本:把段放进该放的地址
config.h 改完,下一步是确认 Makefile 知道自己在为哪个 CPU 编译。典型的 BSP Makefile 开头长这样:
ENDIAN = LE CPU = ARM9 TOOL = ARM_gnu TGT_DIR = $(WIND_BASE)/target BOARD = arm9_bsp CUSTOM_BSP = TRUE include $(TGT_DIR)/h/make/make.$(CPU)$(TOOL) include $(TGT_DIR)/h/make/make.$(BOARD)这里 ENDIAN 和 TOOL 基本固定,真正要关注的是 BOARD 和链接脚本。BOARD 决定了 make 会去找哪个 board 目录下的规则,CUSTOM_BSP 告诉编译系统你用的是自己的 BSP,而不是编译器自带的默认板级配置。
链接脚本是另一个容易“幸存者偏差”的地方。很多模板里用的是 bootrom.lds,它规定哪些段放 ROM、哪些段在启动时拷贝到 RAM。下面是一段简化的 ARM9 ROM 布局:
/* 链接脚本片段,示意 ROM 和 RAM 区 */ ROM_BASE = 0x00000000; RAM_BASE = 0x00100000; SECTIONS { .startup : { *(.startup) } > ROM_BASE .rodata : { *(.rodata) } > ROM_BASE .data : { *(.data) } > RAM_BASE }链接脚本其实描述的是“bootrom 解压前的地址”和“解压后要拷贝到的地址”。如果这里出现地址重叠,编译大概率报错,或者生成的 bin 文件烧进去后直接跳飞。我习惯在做任何改动前先备份一份原始 .lds,因为后面调整内存布局时很容易改来改去就不知道哪个版本能跑。
3.3 编译三步:先 bootrom、再 vxWorks、最后检查符号表
配置就绪后,编译顺序不要乱。先编译 bootrom,再编译 vxWorks,最后用符号表确认关键函数是否被链接进去了。
cd $WIND_BASE/target/config/arm9_bsp make clean make bootrom make vxWorksbootrom 会生成 bootrom.bin,vxWorks 生成的是带符号表的 ELF 文件。编译过程中如果遇到 undefined symbol,不要急着去网上找答案,先看是不是某个驱动文件没有编进 Makfile 的 MACH_EXTRA 列表。常见做法是在 Makefile 里显式加上:
MACH_EXTRA = sysSerial.o sysNet.o这一步是很多人忽略的:VxWorks 的 Makefile 不会因为你放了 sysSerial.c 在目录里就自动编进去。我一般会在编译前 grep 一下 MACH_EXTRA,看看目标文件有没有遗漏。编译过后用 nm 或 objdump 查一下 vxWorks 里有没有 sysHwInit、usrInit 这些函数,确保没有链接错版本。
4. 中断与驱动接入:让 ARM9 的定时器、串口和网卡真正“活”过来
4.1 中断控制器和系统时钟:sysHwInit 里必须做的一件事
BSP 的最终目的是让 VxWorks 内核能够响应中断,而 ARM9 上的中断控制器 VIC 通常需要在系统启动早期就被正确映射。VxWorks 把板级初始化分成 sysHwInit 和 sysHwInit2 两个阶段,前者调用时系统还很“裸”,后者调用时基础服务已经可用。
void sysHwInit(void) { intLocalDisable(); /* 先关全部中断 */ /* 配置定时器为内核 tick 提供时钟 */ sysTimerInit(); /* 初始化串口硬件 */ sysSerialHwInit(); }这段代码最重要的地方是 intLocalDisable 必须在最前面。如果某个外设处于中断挂起状态,而又没有在初始化时清掉,后续执行 intConnect 时会出现“灵异”中断,系统刚启动就进入异常处理。sysTimerInit 通常操作的是 ARM9 芯片内部的定时器模块,要确认定时器时钟源是不是和 CPU 主频一致,不然系统时间会成倍漂移。
这里有一个容易被忽略的点:sysHwInit 里不能调用 malloc、printf 这类依赖内核服务的函数。因为该阶段 VxWorks 内核的内存池还没有建立。很多新手把打印信息直接写进 sysHwInit,结果函数执行到一半就访问空指针。正确做法是先只操作寄存器,把状态记录到全局变量,等串口驱动真正起来后再输出。
4.2 串口驱动和网卡:从设备树到 END 接口
串口驱动在 VxWorks 里通常作为 tty 设备注册。一个最小实现要覆盖打开、读、写、ioctl 四类操作。以最常用的 16550 兼容 UART 为例,初始化函数至少要做波特率分频、收发使能、FIFO 清空三件事:
void sysSerialHwInit(void) { volatile UINT32 *uart = (UINT32 *)UART_BASE_ADRS; uart[UART_IBRD] = UART_CLK_RATE / (16 * CONSOLE_BAUD_RATE); uart[UART_FBRD] = 0; uart[UART_LCRH] = (3 << 5) | 0x10; /* 8 位数据,使能 FIFO */ uart[UART_CR] = 0x301; /* 使能 UART、TX、RX */ }串口驱动能通,BSP 就成功了一半,因为后续所有调试都要靠它打印日志。网卡驱动则会复杂一些,VxWorks 标准接口是 END(Enhanced Network Device)。写 END 驱动时,通常要把探测函数挂到 sysNet.c 里,并填写设备名、总线基址、中断号和 MAC 地址来源。伪代码大致如下:
END_OBJ *sysNetsioDevCreate(void) { END_OBJ *pEnd; pEnd = (END_OBJ *)memAlloc(sizeof(END_OBJ)); /* 填入寄存器和中断号 */ pEnd->etherName = "arm9net"; pEnd->ioBase = (void *)NET_BASE_ADRS; pEnd->irq = NET_IRQ; return pEnd; }网卡驱动最麻烦的不是寄存器读写,而是 PHY 的复位时序。很多板子要求上电后必须给 PHY 芯片几百毫秒的稳定时间,如果 sysHwInit 过早访问 PHY,读回来的 ID 都是 0xFFFF,VxWorks 会直接报“ether link down”。我一般会把 PHY 复位放在 sysHwInit2 里,或者手工延时后再去探测,效果比改驱动代码快得多。
5. 避坑/常见问题:BSP 移植中最容易翻车的五个环节
5.1 串口与内存:最容易把系统跑飞的三个点
现象:上电后用示波器看 CS0 片选是有波形的,但串口连一个字符都不向外发,整个板子像砖头一样。
原因:十有八九是内存控制器参数没配对。ARM9 的 SDRAM 初始化需要按芯片手册设置行列地址数、CAS 延迟、突发长度。很多模板代码里这些值是用宏定义的,从 A 板换到 B 板时,如果位宽从 16 位变成 32 位,对应的宏却没同步改,内存访问就会错位,代码取指返回的全是乱码。
解决:先不要跑 VxWorks,写一个只有几十行汇编的裸机程序,完成内存控制器初始化后向 SDRAM 写一个固定模式再读回来,确认读写一致后再回 BSP。这个办法虽然“土”,但能最快定位是内存参数还是后续启动代码的问题。
现象:串口有输出,但输出内容是每隔几个字符夹带一个 ff,或者波特率明显不对。
原因:UART_CLK_RATE 和实际外设时钟不一致。ARM9 上 UART 的时钟经常由 PLL 分频得到,有人直接把 CPU_CLK_RATE 当串口时钟填进去,结果分频数算错。
解决:查阅芯片手册里 UART 时钟树,或者用示波器量一下 TX 引脚上每一 bit 的宽度。常见做法是先写一个循环发送 0x55 的裸机串口程序,调整 UART_CLK_RATE 直到波形稳定,再把这组参数填回 config.h。
现象:系统启动后过了几秒突然复位,或者访问某个地址直接进入 abort。
原因:中断向量表没有正确重映射。ARM9 有两种中断向量模式:高地址和低地址。VxWorks 默认用低地址 0x00000000,但有些板子把 Flash 映射到高地址,导致中断发生后跑到了错误地址。
解决:在 romInit.s 里设置 CP15 的 V 位,并确认 config.h 中的中断向量地址与链接脚本一致。不要以为这是汇编细节就不管,VxWorks 内核对 ARM 异常向量地址有硬性要求。
5.2 网络与编译:网卡探测不到、镜像下载超时的两个常见原因
现象:bootrom 起来后可以烧写镜像,但进入 VxWorks 后执行 ifconfig 看不到 eth0,只有 lo0。
原因:END 驱动没有注册成功。常见原因是 sysNet.c 里的设备创建函数没有被调用,或者网络设备初始化宏 NET_IF_NUM 没在 config.h 里启用到 1。
解决:检查 config.h 中的 INCLUDE_END 是否定义,同时确认 Makefile 的 MACH_EXTRA 里有 sysNet.o。如果两端都对,那再查中断号,很多 ARM9 芯片的网卡中断不是默认 IRQ,需要从原理图反推。
现象:用 Bootrom 通过网口下载 vxWorks 镜像时,总是下载到 30% 左右就超时,偶尔又完全正常。
原因:网卡驱动的收发 buffer 地址和内存布局冲突,或者 DMA 缓冲区没有按 cache line 对齐。这属于典型的驱动 bug,和 BSP 链接脚本也有关系。
解决:在 sysNet.c 里给 DMA 描述符和 buffer 分配内存时,显式做 cache 对齐,例如把地址按 32 字节向上取整。另一个常见做法是先把网卡驱动的 cache 功能暂时关掉,确认能连续下载后再逐步开启 DMA 和 cache 优化。跳过这一步,之后遇到的随机丢包会让你怀疑人生。
6. 验证与进阶:用串口命令行和 Wind Shell 确认 BSP 到底稳不稳
6.1 先用这三条命令给 BSP 做“体检”
BSP 能启动只是第一步,能不能长时间稳定运行还要靠调试命令验证。串口终端进入 VxWorks Shell 后,我习惯先敲三个命令:
-> devs -> i -> m 0x00100000 16devs 会列出当前所有设备,如果节 4.2 里的 tty 设备和网卡都出现在列表里,说明驱动注册成功。i 命令显示系统任务列表,重点看是否有 tUsrRoot 任务在死循环,或者异常任务导致系统空转。m 命令用来读内存,随便挑一段地址读出数据,可以快速确认内存总线是否还有隐藏的时序问题。
这三个命令都有对应的拼写错误:VxWorks Shell 是区分大小写的,devs 不能写成 dev。如果连 devs 都敲不出来,说明串口驱动还停留在“能打印”但“不能接收”的状态,要回 sysSerialHwInit 检查接收中断使能。
6.2 把启动日志抓下来对照
进阶操作是保存一份完整的启动日志。具体做法是把串口工具的输出重定向到文件,然后启动板子,等 VxWorks 完全起来后,用 Shell 里的 logout 命令切断。注意不要只看最后的启动 banner,重点比对以下三个时间点:bootrom 首次打印、内存初始化完成、驱动 load 完成。
我曾在某款 ARM9 芯片上遇到一个很隐蔽的问题:启动日志显示设备全部初始化成功,但运行约十分钟后系统时间突然跳变。反复排查后发现是系统 tick 定时器的预分频值被 config.h 里的 AUX_CLK_RATE 影响,而日志里根本看不出这种问题。从那以后我每次换 BSP,都会强制跑一遍 24 小时稳定性脚本,把 Shell 的命令行输出保存下来,再用脚本对比每个阶段的时间戳。如果定时器有漂移,脚本会自动标红。这个方法虽然简单,但真能逼出很多“平时没事、压力打死”的 BSP 隐患。
希望这份基于 ARM9 的 VxWorks BSP 拆解能帮到你。整套可复现的 config.h、驱动模板和编译脚本我都按这个思路整理在下载包里了,你拿回去对着芯片手册改一改,比从空目录开始手搓要省一半时间。
本文还有配套的精品资源,点击获取