3、ARM Cortex-M系列:内核架构、存储器映射、异常与中断

好,咱们今天来聊聊ARM Cortex-M系列。说实话,这是嵌入式开发里我最喜欢的一个内核系列。为什么?因为它简单、高效,而且特别适合做实时控制。我在项目里用过M0、M3、M4,甚至M7,每个都有自己的脾气。但核心的东西,其实都差不多。

3.1 内核架构概览

Cortex-M系列是ARM公司专门为微控制器设计的。它跟以前的ARM7、ARM9不一样。那些是ARM架构,需要跑操作系统才能发挥威力。Cortex-M不一样,它天生就是为裸机或RTOS设计的。

我个人习惯把Cortex-M的内核分成三块来看:

  • 处理器核心:就是执行指令的地方。M0是冯诺依曼结构,M3/M4/M7是哈佛结构。说白了,M0的指令和数据共用一条总线,M3以上分开走,速度更快。
  • 嵌套向量中断控制器(NVIC):这是Cortex-M的杀手锏。它管理所有中断,支持优先级嵌套。我记得第一次用M3的时候,发现中断响应只要12个时钟周期,当时就震惊了。
  • 调试与跟踪系统:比如SWD、JTAG、ETM等。这些在调试时特别有用。我曾经在一个项目里,靠ETM抓到的指令流,硬是找到了一个随机死机的bug。

核心要点:Cortex-M是32位RISC处理器,采用Thumb-2指令集。这意味着它既能用16位指令节省空间,也能用32位指令提高性能。你想想看,一条指令就能完成乘加运算,这在M0上可做不到。

3.2 存储器映射

存储器映射,说白了就是给芯片里的各种资源分配地址。Cortex-M的地址空间是4GB(2^32)。ARM把这4GB分成了几个区域。每个区域有固定的用途。

我建议你记住下面这张表。这是Cortex-M的标准映射,几乎所有芯片都遵循这个规则。

地址范围 区域名称 用途
0x00000000 - 0x1FFFFFFF Code区 存放程序代码,也可以映射到Flash或SRAM
0x20000000 - 0x3FFFFFFF SRAM区 片内RAM,存放变量和堆栈
0x40000000 - 0x5FFFFFFF 外设区 所有外设寄存器都在这里,比如GPIO、UART、TIM
0x60000000 - 0x9FFFFFFF 外部RAM区 扩展的外部存储器
0xA0000000 - 0xDFFFFFFF 外部设备区 扩展的外部设备
0xE0000000 - 0xE00FFFFF 系统控制区 NVIC、MPU、SysTick等内核外设
0xE0100000 - 0xFFFFFFFF 保留/厂商特定 芯片厂商自定义

嗯,这里要注意。Code区是从0x00000000开始的。但很多芯片会把Flash映射到这里。比如STM32,它的Flash起始地址就是0x08000000,但通过别名映射,你从0x00000000也能访问到。我曾经在调试时,发现程序跑飞了,结果是因为中断向量表没放在正确的位置。后来我习惯把向量表放在0x00000000,省心。

小技巧:在写链接脚本(Linker Script)时,一定要确认Flash和RAM的起始地址。不同芯片不一样。比如NXP的LPC系列,RAM可能从0x10000000开始。别想当然。

3.3 异常与中断

这是Cortex-M最强大的地方。异常和中断,本质上是一回事。异常是内部事件(比如除零、系统调用),中断是外部事件(比如按键、定时器)。Cortex-M把它们统一管理了。

Cortex-M有一个中断向量表。它放在地址0x00000000处。前16个是系统异常,后面的都是外部中断。具体数量看芯片,M0最多32个,M3/M4最多240个。

我列一下常用的系统异常:

  • Reset:优先级-3,最高。芯片复位后第一个执行的。
  • NMI:优先级-2,不可屏蔽中断。一般用于紧急情况,比如电源掉电。
  • HardFault:优先级-1,硬件错误。当其他异常处理不了时,就交给它。我在项目中遇到最多的就是HardFault,通常是数组越界或野指针。
  • SVCall:系统服务调用。RTOS里常用,比如任务切换。
  • PendSV:可挂起的系统调用。也是RTOS用的,用于延迟处理。
  • SysTick:系统滴答定时器。RTOS的心跳,裸机里也常用作延时。

避坑指南:我曾经在一个项目里,因为中断优先级配置不当,导致高优先级中断一直抢占低优先级中断,低优先级的中断永远得不到执行。后来我查了手册才发现,Cortex-M的优先级数值越小,优先级越高。跟直觉相反。记住:0是最高优先级,255是最低。

3.4 中断处理流程

Cortex-M的中断处理,跟传统ARM不一样。传统ARM需要软件保存现场(压栈),Cortex-M是硬件自动完成的。这大大提高了中断响应速度。

流程是这样的:

  1. 中断发生,硬件自动压栈:PC、LR、PSR、R0-R3、R12。一共8个寄存器,32字节。
  2. 硬件从向量表中取出中断服务函数(ISR)的地址,跳转执行。
  3. ISR执行完毕,执行一条特殊的返回指令(使用EXC_RETURN机制)。
  4. 硬件自动出栈,恢复现场,继续执行原来的程序。

你想想看,整个过程硬件帮你做了大部分工作。你只需要写ISR函数就行。但要注意,ISR要尽量短。我习惯在ISR里只设置一个标志位,然后退出。真正的处理放在主循环里。

下面是一个简单的SysTick中断示例:

// SysTick中断服务函数
void SysTick_Handler(void)
{
    // 只做最少的操作
    tick_count++;
    
    // 如果用了RTOS,这里可以触发任务切换
    // 但不要在ISR里做复杂运算
}

// 主循环里处理
int main(void)
{
    uint32_t last_tick = 0;
    
    // 配置SysTick,每1ms中断一次
    SysTick_Config(SystemCoreClock / 1000);
    
    while(1)
    {
        if(tick_count - last_tick >= 1000)
        {
            last_tick = tick_count;
            // 每秒执行一次的任务
            LED_Toggle();
        }
        
        // 其他任务...
    }
}

核心要点:Cortex-M的中断延迟非常低。M0大约16个时钟周期,M3/M4大约12个时钟周期。这意味着你可以在1MHz的时钟下,实现60KHz以上的中断频率。这在电机控制、电源管理等领域非常有用。

3.5 异常优先级与嵌套

Cortex-M支持中断嵌套。也就是说,一个高优先级的中断可以打断正在执行的低优先级中断。这是通过NVIC的优先级分组实现的。

优先级分组有两种:

  • 抢占优先级:决定是否可以打断。数值越小,优先级越高。
  • 子优先级:当抢占优先级相同时,子优先级决定谁先执行。但不会打断。

我建议你在项目开始时,就确定好优先级分组。比如STM32,默认是4位抢占优先级,0位子优先级。也就是16级抢占。我个人习惯用这个配置,简单明了。

配置代码如下:

// 设置优先级分组:4位抢占优先级,0位子优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);

// 使能USART1中断,抢占优先级设为2
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

小技巧:在RTOS中,通常把SysTick的优先级设为最低。因为SysTick中断会频繁触发,如果优先级太高,会阻塞其他中断。我曾经把SysTick优先级设为0,结果所有外设中断都被它堵死了。嗯,血的教训。

3.6 实战经验总结

最后,我分享几个我在项目中积累的经验:

  • 中断向量表重定位:有些芯片支持把向量表放在RAM里。这在做IAP(在线升级)时非常有用。你可以把新的程序下载到Flash的其他区域,然后修改向量表地址,指向新程序。注意,修改后要重新使能中断。
  • 中断优先级不要用满:我建议留出1-2个最高优先级给系统异常(比如HardFault)。这样即使你的程序崩溃了,还能通过HardFault处理程序打印一些调试信息。
  • 使用位带操作:Cortex-M3/M4支持位带操作。你可以把某个寄存器的某一位映射到一个单独的地址。这样操作位就像操作一个变量一样简单。我在控制GPIO时特别喜欢用这个。
  • MPU(内存保护单元):M3以上支持MPU。它可以保护关键内存区域不被非法访问。我在做安全关键系统时,一定会启用MPU。比如把栈区设为不可执行,防止栈溢出攻击。

好了,关于Cortex-M的内核架构、存储器映射和异常中断,就讲到这里。这些东西是嵌入式开发的基石。你理解了它们,后面学RTOS、驱动开发都会轻松很多。下一章,我们聊聊启动流程,看看芯片上电后到底发生了什么。