☰
VxWorks在ARM9上的BSP移植:从启动流程到常见坑
2026/10/18 12:37:23 网站建设 项目流程

简介:面向嵌入式开发者的VxWorks基于ARM9(S3C2410X)的板级支持包(BSP)资源,适用于工业控制、航空电子等嵌入式系统开发者,重点解决硬件平台启动、驱动适配与内核移植过程中的关键问题。压缩包共37个文件,大小仅1000KB,包含10个C源码、9个头文件、7个目标文件、3个汇编文件以及Makefile、README等,覆盖串口、中断控制器、定时器、CS8900A网卡等外设驱动。包内还包含bootrom启动镜像、s3c2410x配置文件与初始化代码,可对照研究VxWorks从ROM启动、设置堆栈、跳转到内核的完整流程。通过阅读源码与配置,可深入理解S3C2410X内存管理、中断服务、系统时钟和电源管理等BSP开发要点。目前已有189人学习,对于希望在ARM9平台上搭建VxWorks系统、理解BSP结构和编写底层驱动的开发者,是难得的实战参考。

1. VxWorks在ARM9上跑不起来的卡点,九成在BSP

一块ARM9板卡、一套VxWorks源码,编译过了、烧录完成了,结果复位后串口只吐了一个字符就没动静——这是很多第一次接触VxWorks移植的人都会撞上的场景。问题往往不在应用层,而在VxWorks和板卡之间那层BSP(Board Support Package,板级支持包)。BSP决定CPU能不能初始化、内存能不能用、中断能不能进来、调试串口能不能说话;ARM9没有x86那样的BIOS兜底,每一块板子都得自己维护这层适配。我按实际移植的顺序把这件事讲透,从BSP的文件构成、两段式启动、最小移植顺序讲到最常见的几个坑,给打算在ARM9上跑VxWorks、或者已经被启动阶段卡住的工程师一条可复现的路径。

2. BSP这套“板级胶水”里到底有什么:文件构成与两段式启动

2.1 BSP目录清单:从romInit.s到sysLib.c,哪几个文件是命根子

经典VxWorks BSP目录打开之后,文件数量看起来吓人,但真正决定生死的没几个。我第一次移植时也试图把每个文件都看明白,后来发现时间和精力都花错了地方。对ARM9这类板卡来说,下面这组文件优先读就够定位九成问题。

文件所处阶段核心职责
romInit.s复位向量入口第一条汇编指令,关闭中断、初始化内存控制器、设置栈指针
config.h编译期配置内存大小、控制台设备、BSP版本宏、Boot line默认值
sysLib.c内核使用提供sysClkRateGet、内存探测、板级信息等基础接口
sysHwInit.c内核启动阶段调用各外设驱动初始化入口,注册设备到I/O系统
Makefile构建规则指定CPU架构、编译器类型、镜像生成目标和链接地址

我一般会先打开romInit.s,看它的内存初始化是否能在Cache关闭、无栈环境下跑通;再看config.h里的地址宏和外设基地址跟板卡原理图能否对上。如果这两处没问题,BSP的骨架基本就是健康的。

2.2 两段式启动:Bootrom先接管Flash,内核再接管内存

ARM9板卡上电后,CPU从复位向量取指,执行romInit.s里的汇编代码,这是Bootrom的起点。Bootrom是一个烧录在Flash或ROM里的最小镜像,任务是把内存控制器初始化好,再把主内核镜像从串口、网口或Flash加载到内存里,最后跳转过去。整个流程没有BIOS参与,所有硬件配置都要BSP自己在最初几十条指令里完成。

值得留意的是,Bootrom阶段做的是最小初始化,VxWorks主镜像被Bootrom加载后,还会重新执行一遍sysHwInit,把串口、网口、定时器、中断控制器等完整初始化一遍。这套两段式设计意味着同一个外设可能被初始化两次,所以写驱动时要注意幂等——重复调用不能导致状态错乱。还有一点很关键:在romInit阶段PC指针跳转到C代码之前,系统连栈都还没建立,内存控制器必须在此之前配置完成,否则后面全是黑匣子。

2.3 为什么ARM9比x86更依赖BSP

x86有标准化的BIOS和ACPI,外设枚举、中断路由、电源管理都有通用协议兜底;ARM9做不到,不同厂商的SoC在中断控制器结构、时钟树、UART寄存器布局、内存控制器时序上几乎全不一样,连同一颗SoC的不同板卡都可能在引脚复用和GPIO分配上不同。BSP就是把这套差异全部消化掉的那一层。

所以BSP移植的工作量并不是“改几个参数”那么轻,它至少要覆盖内存控制器、调试串口、中断控制器、系统定时器、Cache/MMU属性这几个硬骨头。其中MMU属性是最隐蔽的:外设区域如果被映射成可缓存,寄存器写入会被合并或延迟;DMA缓冲区如果被映射成可缓存,网络数据包就会随机错乱。这类问题应用层怎么查都查不到根因,只能在BSP层解决。

3. 从零搭一个ARM9的BSP:最小启动链路、内存映射与首批要改的文件

3.1 第一步:先点亮调试串口,其余外设后面再说

串口是唯一能在早期阶段观察系统状态的通道,BSP移植的起步动作就是让调试串口先开口说话。常见做法是:看原理图确认UART引脚接到SoC的哪一组引脚,查SoC手册配好引脚复用和时钟门控,再找到UART寄存器基址。这一阶段不要调用任何依赖设备注册的printf,因为设备模型还没建立,输出函数得自己写。

我通常会在C入口的最前面挂一个early_putc,直接操作寄存器发送字符:

/* 早期串口输出示例,寄存器基址与偏移以板卡手册为准 */ #define UART_BASE 0x50000000 /* 串口寄存器基址,来自SoC手册 */ #define UTXH (*(volatile unsigned int *)(UART_BASE + 0x20)) /* 发送数据寄存器 */ #define UTRSTAT (*(volatile unsigned int *)(UART_BASE + 0x10)) /* 收发状态寄存器 */ void early_putc(char c) { while ((UTRSTAT & (1 << 2)) == 0) /* 发送FIFO非空时等待 */ ; UTXH = c; } void early_puts(const char *s) { while (*s) early_putc(*s++); }

这里的基址和偏移是示意值,ARM9板上常见做法是先用SoC手册查基址,偏移每家略有差异。UTXH在写之前必须确认对应的状态位含义:有些SoC用bit 0表示发送保持寄存器空,有些用bit 2;对着手册改这一个条件就行。另一个容易翻车的点是波特率:早期阶段尽量用外部晶振直接分频,不要依赖PLL倍频,因为PLL配置本身可能就是你正在调试的部分。逻辑分析仪挂在TX引脚上可以快速验证波特率是否真的符合预期。

3.2 内存控制器与MMU:把物理地址和外设地址摆平

ARM9的BSP里,内存控制器初始化是优先级最高的一件事。很多板卡默认配置能跑起来但只在低速率下稳定,一旦下载大的vxWorks镜像就CRC出错或跑飞,原因就是SDRAM/DDR时序参数没按实际颗粒调整。我会对照内存颗粒手册,把列选通延迟、行预充电时间、刷新周期这几个参数逐个核一遍,先降到最保守档位确认能稳定启动,再慢慢拉高频率。

内存控制器稳定之后,下一个关键点是MMU映射。VxWorks BSP的内存描述符数组负责决定虚拟地址到物理地址的映射关系,以及每个区域的Cache和缓冲策略。常见做法是针对ARM9划分三类区域:内存区开写回Cache提升性能,外设寄存器区必须设成不缓存不缓冲,DMA缓冲区单独处理。示意写法如下:

/* MMU映射描述符示意,属性宏名以当前BSP头文件为准 */ PHYS_MEM_DESC sysPhysMemDesc[] = { { INTCTL_BASE, INTCTL_BASE, 0x1000, /* 中断控制器:虚拟地址、物理地址、长度 */ CACHE_NONE | IO_NON_BUFFERED }, /* 外设区必须不缓存、不缓冲,否则寄存器写操作会被合并或延迟 */ { UART_BASE, UART_BASE, 0x1000, /* 串口寄存器区 */ CACHE_NONE | IO_NON_BUFFERED }, { DRAM_BASE, DRAM_BASE, DRAM_SIZE, /* 内存区 */ CACHE_WRITEBACK }, /* 内存区开写回缓存,用于保证常规代码运行性能 */ };

这段代码要有两个意识:一是每个表项三个地址参数按“虚拟地址、物理地址、长度”的顺序填,映射时基址要对齐到段边界;二是外设区的属性位是整个BSP里最容易抄错的地方,很多工程师把内存区的Cache属性直接复制给了寄存器区,导致控制寄存器写进去像石沉大海。代码里的宏名不同版本可能写法不同,但含义是通用的,以BSP头文件定义为准。

3.3 最小构建命令:make前需要什么,输出怎么检查

BSP文件改好之后,构建环节也有讲究。构建VxWorks的常见做法是通过命令行make,前提是先把交叉编译环境变量导出。不同工具的变量名有差异,但经典套路长这样:

export WIND_BASE=/opt/windriver/vxworks # VxWorks安装根目录,改成实际路径 export CPU=ARMARCH5 # ARM9通常对应ARMARCH5,以BSP Makefile里默认值为准 export TOOL=diab # 编译器类型,BSP同时支持diab和gnu cd /path/to/arm9_bsp make clean make bootrom # 生成烧写到Flash的Bootrom镜像 make vxWorks # 生成主内核镜像

CPU变量是移植时最容易被忽略的:不同VxWorks版本对ARM9架构的命名不同,有的叫ARMARCH5,有的用更细的变体名,写错的结果是编译出一堆奇怪报错或者镜像根本无法启动。TOOL变量决定用哪套编译器,BSP里旧代码如果用了某编译器特有的内联汇编或扩展语法,换编译器后要重新过一遍。构建产物检查也有顺序:先看bootrom.bin是否生成,再看vxWorks镜像大小是否与内存布局匹配,最后确认烧写地址与BSP里ROM_BASE_ADRS宏一致。地址不一致的典型表现是烧录成功但复位后完全没输出,因为CPU从错误的地址取指了。

4. 让Bootrom和内核真正跑起来:下载调试与三组必调参数

4.1 下载连接的选择:串口、网口、烧Flash三种路径

Bootrom烧进Flash之后,接下来要把vxWorks主镜像从开发机搬到目标板内存里。加载路径的选择会影响整个调试效率,我根据自己的经验按场景推荐:

加载方式速度适用阶段前提条件
串口下载慢,适合小镜像Bootrom刚跑通的早期阶段Bootrom串口驱动已通,boot line正确
TFTP/FTP网络下载快日常开发和反复调试网口驱动已通,IP配置正确,开发机开TFTP服务
直接烧写Flash无需开发机发布阶段和产线烧录Flash烧写工具和正确的烧写地址

对于第一次接触ARM9板卡的人来说,建议从串口下载起步,因为网络栈本身也是BSP的一部分,网络加载出问题时很难区分是Bootrom问题还是应用问题。串口路径只依赖UART驱动,排查链路短得多。

4.2 boot line六段参数:每个字段失败时看哪里

VxWorks的启动参数集中在一行boot line里,它是网口和串口下载能否成功的关键。常见格式如下:

fei(0,0)host:/tftpboot/vxWorks h=192.168.1.100 e=192.168.1.90 u=target pw=target f=0x00

逐段拆解:

字段含义常见失败点
fei(0,0)启动设备类型、控制器号、单元号网卡驱动名与BSP不匹配
host:/tftpboot/vxWorks开发机主机名、文件路径TFTP根目录配置错或文件名打错
h=开发机IP地址和目标板不在同一网段
e=目标板IP地址写错或和开发机冲突
u= / pw=下载用的FTP用户名密码用TFTP下载时通常可忽略
f=下载参数标记开发期写成0,发布时按需求加自动启动位

排查网络下载失败时,我一般按物理链路、IP可达性、TFTP路径、文件权限这个顺序走,不要一上来就怀疑Bootrom的网卡驱动。先用开发机ping目标板,通了再在当前机器的TFTP根目录下手动取一次文件,确认路径和权限没问题,最后才看Bootrom打印的IP信息是否和boot line一致。网口协商速度不一致导致的下载卡顿也很常见,固定到百兆全双工能减少这类玄学问题。

4.3 用Shell给BSP做第一次“体检”:i、devs、sysClkRateGet

下载并启动vxWorks镜像后,先别急着跑应用,应该先确认BSP层是否真的工作正常。最直接的方式是进入Target Shell敲几条命令:

-> i -> devs -> sysClkRateGet()

i命令列出所有任务,如果能看到tJobTask、tExcTask等系统任务,说明调度器、定时器中断和任务创建机制都正常。devs命令列出设备树上的所有设备,能看见串口设备名和网口设备名,说明sysHwInit里各驱动初始化成功并完成注册。sysClkRateGet()返回系统节拍率,对照config.h里配置的期望值,能判断时钟源和定时器分频是否正确。这三条命令一次通过,BSP的基本盘就算稳了。

5. ARM9上BSP移植的六个常见坑:现象、原因与解法

5.1 串口只吐出一两个字符就卡死

现象是上电复位后能看到几个字符输出,然后就再无任何反应。之前我遇到过一例,一度怀疑是晶振起振慢,折腾好几天。

原因是早期串口初始化代码在内存控制器初始化之前就被调用,代码和栈都还处于半可用状态;也可能是串口时钟被切到PLL后波特率跳变,后续输出全部变成乱码或直接卡死。解决方法是严格保证调用顺序:先把内存控制器配好,栈建立完成,再初始化串口;波特率在调试早期固定用外部晶振直连分频,不要依赖PLL输出。手边有逻辑分析仪的话直接看TX波形,一分钟就能确认波特率是否符合配置值。

5.2 Bootrom能引导,下载后vxWorks镜像跑飞

现象是Bootrom正常启动,网络下载也能完成,但跳转到vxWorks入口后系统毫无输出,或输出乱码后复位。

原因通常是DDR/SDRAM时序参数用了板卡默认的保守值或错误值:小镜像时内存访问压力小,看起来一切正常;大镜像加载和运行时出错。解决方法是翻内存颗粒手册,把列选通延迟、行预充电时间、刷新周期逐项核对。先把内存控制器时钟降到最低档跑通内存自检,确认稳定后再分档提升。这一步偷懒的话,后面遇到的随机死机问题会让你加倍补回来。

5.3 中断永远不触发:VIC和总开关都没打开

现象是应用层注册好了ISR,外部事件确实发生了,但服务函数一次都不执行。

原因是ARM9的中断链路有两级开关:SoC自带的中断控制器(VIC)默认屏蔽所有中断源,同时ARM核的CPSR里总中断位也可能是关闭状态。常见误用是只调了intConnect建立连接,漏掉了intEnable去打开具体中断源,或者BSP初始化时压根没处理CPSR。解决办法顺链路查:先确认CPSR的中断位状态,再查VIC中对应中断号是否使能,最后读VIC的pending寄存器确认事件是否到达。用状态寄存器说话,比盯着ISR里打断点高效得多。

5.4 网络DMA收包全是乱数据:Cache一致性被忽略

现象是ping能通但应用收上来的数据偶发错乱,时好时坏,抓包看数据本身是对的。

原因是DMA缓冲区的内存被MMU映射成了可缓存,DMA控制器写入内存的数据还留在Cache里,CPU读到的旧数据;反过来CPU写了缓冲区后Cache没有回写,DMA发出去的也是脏数据。解决办法一是把DMA缓冲区放到uncacheable地址区域,二是在每次DMA操作前后显式做Cache flush和invalidate。早期调试阶段我会把Cache模式先调成最保守的直写,确认数据正确之后再考虑开写回优化性能,不要一上来就追求最优配置。

5.5 外设寄存器写进去不生效:MMU把外设区映射成了可缓存

现象是寄存器读出来看着是写成功了,但外设行为完全不变,或者配置偶尔生效偶尔无效。

原因是sysPhysMemDesc里外设区被标成了可缓存、可缓冲,CPU的写操作被合并或延迟,实际到达外设的时序已经变了。解决方法是把外设区属性改成不缓存、不缓冲,并用volatile修饰寄存器指针,防止编译器优化掉看似多余的访问。这个坑排查起来隐蔽,因为频繁读写看起来正常,只有逻辑分析仪抓总线时序才能看到端倪。

5.6 网络下载卡在“Loading”不动

现象是Bootrom能识别启动设备,能解析IP,但停在Loading字样上不再前进。

常见原因是TFTP路径或文件名写错、开发机防火墙拦截了端口、目标板和开发机不在同一子网、网口协商速率不匹配。解决办法先做减法:在开发机本地把boot line里的文件路径用tftp命令取一次,能取通则问题不在路径和权限;再确认目标板与开发机能互相ping通,如果通了但下载还是卡,换交叉网线并固定网口速率;最后把Bootrom启动阶段打印打开,看它实际请求的IP和文件路径是什么,和boot line期望值逐一对比。

6. 用系统时钟计数给BSP做体检,移植少走弯路

6.1 先把时钟链路当第一位:PLL、预分频、定时器中断是一条链

BSP移植过程中,最不容易被察觉但又影响全局的是时钟链路。串口波特率错了能被看出来,网口速率错了能通过丢包感知,但系统节拍不对时,表面特征是任务调度偏慢、延时函数时间不准,数字层面的现象往往被误判为应用逻辑问题。更麻烦的是,时钟链路跨越SoC的PLL配置、外设时钟分频、系统定时器驱动、中断控制器使能四个环节,任何一个环节出错,表现都可能相似。

我现在的习惯是不急着验证业务代码,而是先写一个小函数做时钟体检。原理很简单:VxWorks内部有一个由系统定时器中断驱动的tick计数器,如果tick的增速和配置的节拍率对得上,说明PLL到定时器再到中断的整条链路是通的。

6.2 写一个自检函数验证tick与频率

下面这个函数在BSP启动完成、Target Shell可用之后执行,通过忙循环前后读取tickGet的差值来判断时钟链路健康状况:

#include <vxWorks.h> #include <tickLib.h> #include <sysLib.h> void bsp_clock_selfcheck(int loops) { int rate = sysClkRateGet(); /* BSP上报的系统节拍率,单位Hz */ ULONG start = tickGet(); /* 记录起始tick计数 */ int i; for (i = 0; i < loops; i++) { tickGet(); /* 忙循环中反复读取tick,让定时器中断有机会累加计数 */ } ULONG span = tickGet() - start; printf("rate=%d Hz, loops=%d, tick span=%d\n", rate, loops, span); }

解读输出结果时,重点是看tick span是否符合量级预期:loops设成一百万,span如果远小于理论值,说明定时器中断没有进,或者PLL时钟源没生效,优先级靠前的中断被屏蔽了;span忽大忽小,说明中断存在明显的优先级抖动或持续被更高优先级中断打断;rate这个值和config.h里的期望值直接对应,差一个数量级就意味着预分频配错了。这个自检函数我会直接写进BSP验证脚本里,每次改完时钟相关配置先跑一遍再往下走。

这套体检流程看起来多花了十分钟,但它是整个BSP移植里性价比最高的投入。现在每次拿到新板卡,我的第一动作已经固定成“点亮串口、确认内存、跑时钟自检”三件事,前两件保证能开口说话,第三件保证说话的节奏是对的。时钟链路没问题之后再去碰网口和外部总线,踩坑概率会小很多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询