第四章:ARM端裸机开发——启动流程、中断与定时器

各位同学,今天我们来聊聊ARM端裸机开发的核心内容。说实话,很多做FPGA的工程师一听到“裸机”两个字就头大,觉得那是软件工程师的活。但在我多年的项目经验里,FPGA和ARM的协同设计,恰恰需要你两边都懂一点。

裸机开发,说白了就是没有操作系统,直接操作硬件。你想想看,在工业控制、高速数据采集这些场景下,操作系统的那点延迟可能就让你丢数据了。所以,掌握裸机开发,是ARM与FPGA协同工作的基本功。

4.1 ARM Cortex-A9/A53 裸机启动流程

ARM芯片上电后,到底干了些什么?我刚开始做的时候也一头雾水。其实流程很清晰,就三步:

  1. 复位向量跳转:CPU上电后,PC指针直接指向0x00000000(或者0xFFFF0000,取决于配置)。这个地方放的就是复位向量。
  2. 初始化C运行环境:包括设置栈指针SP、清零BSS段、拷贝数据段。这一步不做,你的全局变量全是乱的。
  3. 跳转到main函数:一切就绪后,调用你的主程序。

嗯,这里要注意一个细节:A9和A53的启动模式不太一样。A9通常从片内SRAM启动,而A53往往需要先加载FSBL(First Stage Boot Loader)。我在一个Zynq项目里就踩过这个坑——以为A53和A9一样直接跑,结果死活起不来。

核心要点:裸机启动的入口文件通常是startup.S,里面定义了中断向量表和复位处理函数。别偷懒用自动生成的,自己手写一遍才能理解底层逻辑。

4.2 中断向量表配置

中断向量表,说白了就是一张“谁来了该找谁”的表格。ARM Cortex-A系列支持两种模式:

  • Normal Vector Table:基地址0x00000000
  • High Vector Table:基地址0xFFFF0000

我个人习惯用Normal模式,因为简单。配置中断向量表,核心就两步:

  1. 在startup.S中定义向量表
  2. 通过CP15协处理器设置VBAR寄存器
// startup.S 示例
.section .vectors, "ax"
.global _start
_start:
    ldr pc, =Reset_Handler      // 复位
    ldr pc, =Undef_Handler      // 未定义指令
    ldr pc, =SVC_Handler        // 软件中断
    ldr pc, =PrefAbort_Handler  // 预取中止
    ldr pc, =DataAbort_Handler  // 数据中止
    ldr pc, =NotUsed_Handler    // 保留
    ldr pc, =IRQ_Handler        // 普通中断
    ldr pc, =FIQ_Handler        // 快速中断

设置VBAR的代码也很简单:

// 设置中断向量表基地址
void vector_table_init(void)
{
    uint32_t vbar = (uint32_t)&_start;
    __asm volatile("mcr p15, 0, %0, c12, c0, 0" : : "r"(vbar));
}

避坑指南:我曾经在调试一个中断不响应的问题时,查了整整两天。最后发现是VBAR没设置对,向量表地址写错了。记住:VBAR必须32字节对齐!

4.3 通用定时器初始化

ARM Cortex-A系列内部有一个通用定时器(Generic Timer),它比外挂的定时器精准得多。为什么?因为它直接挂在系统总线上,不受外设总线延迟影响。

初始化流程:

  1. 使能定时器计数器
  2. 设置比较值
  3. 使能中断(如果需要)
// 通用定时器初始化
void generic_timer_init(uint32_t interval_us)
{
    uint32_t freq;
    uint32_t compare_val;
    
    // 获取定时器频率
    __asm volatile("mrc p15, 0, %0, c14, c0, 0" : "=r"(freq));
    
    // 计算比较值
    compare_val = (freq / 1000000) * interval_us;
    
    // 设置比较值
    __asm volatile("mcr p15, 0, %0, c14, c2, 1" : : "r"(compare_val));
    
    // 使能定时器
    uint32_t ctrl = 1;  // 使能位
    __asm volatile("mcr p15, 0, %0, c14, c2, 0" : : "r"(ctrl));
}

你想想看,这个定时器精度能达到纳秒级,做FPGA与ARM的时序同步时特别好用。我在一个高速AD采集项目里,就是用这个定时器来触发DMA传输的。

4.4 看门狗初始化

看门狗,说白了就是一个“保命”的机制。程序跑飞了?死循环了?看门狗会帮你复位系统。

ARM Cortex-A系列的看门狗通常集成在SCU(Snoop Control Unit)里,或者使用私有的SP805模块。初始化步骤:

  1. 解锁看门狗寄存器
  2. 设置超时时间
  3. 使能看门狗
// 看门狗初始化(以SP805为例)
void wdt_init(uint32_t timeout_ms)
{
    // 解锁
    *(volatile uint32_t *)0xF8000000 = 0x1ACCE551;
    *(volatile uint32_t *)0xF8000004 = 0xE53C1192;
    
    // 设置加载值
    uint32_t load_val = (timeout_ms * 1000) / 16;  // 假设时钟周期16us
    *(volatile uint32_t *)0xF8000008 = load_val;
    
    // 使能看门狗
    *(volatile uint32_t *)0xF800000C = 0x3;  // 使能+自动复位
}

重要提醒:看门狗一旦使能,就必须在超时前“喂狗”。喂狗操作很简单,往控制寄存器写任意值即可。但要注意:不要在中断服务函数里喂狗!我曾经见过一个同事在IRQ里喂狗,结果主程序死循环了,看门狗却一直复位不了。

知识体系总览

下面这张图,是我画的一个整体框架。你可以把它当作本章的“地图”:

ARM裸机开发核心知识体系 启动流程 中断向量表 通用定时器 看门狗 协同 复位向量→C环境→main VBAR设置→中断服务 频率获取→比较值→使能 解锁→超时设置→使能→喂狗

这张图把四个核心模块串起来了。你看,启动流程是基础,中断向量表是桥梁,定时器提供时间基准,看门狗保证系统可靠性。它们之间是环环相扣的。

好了,这一章的内容就到这里。记住:裸机开发没有想象中那么难,关键是动手去写代码、去调试。我在带团队的时候,经常让新人从裸机启动开始练手,把底层摸透了,上层应用才能写得稳。

课后练习:试着在QEMU模拟器上跑一个裸机程序,实现定时器中断点亮LED。遇到问题不要慌,先检查向量表地址对不对,再看中断使能寄存器有没有配好。


专注资料整理