4. MSP430中断系统:中断向量表、中断优先级、外部中断配置、定时器中断

中断系统,说白了就是微控制器的“紧急事务处理机制”。

我刚开始用MSP430那会儿,觉得中断就是个“后台任务”。后来踩过坑才明白——中断用不好,整个系统都会乱套。今天咱们就把中断系统彻底讲透。

4.1 中断向量表——中断的“通讯录”

每个中断源都有一个固定的入口地址。这些地址排成一张表,就是中断向量表。

MSP430的中断向量表放在Flash的最高地址区域。比如MSP430F149,向量表从0xFFE0开始,到0xFFFF结束。

关键点:每个中断向量占2个字节,存放的是中断服务函数的入口地址。

我见过不少新手直接往向量表里写数字。千万别这么干!用编译器提供的宏定义或者#pragma指令,既安全又省心。

举个例子,定时器A的中断向量是这样定义的:

// IAR编译器写法
#pragma vector = TIMERA0_VECTOR
__interrupt void Timer_A0_ISR(void)
{
    // 中断处理代码
}

// GCC编译器写法
__attribute__((interrupt(TIMERA0_VECTOR)))
void Timer_A0_ISR(void)
{
    // 中断处理代码
}

嗯,这里要注意:不同编译器语法不一样。我习惯用IAR,但不管用哪个,原理都一样。

4.2 中断优先级——谁先处理?

MSP430的中断优先级是固定的。说白了,中断向量号越小,优先级越高。

中断源 向量地址 优先级
复位 (Reset) 0xFFFE 最高
NMI 0xFFFC 次高
Timer_B7 0xFFF8 ...
Timer_A3 0xFFF0 ...
UART接收 0xFFE8 较低
... ... ...

你想想看,复位优先级最高,这很合理——系统都乱了,当然得先复位。

我的经验:别在低优先级中断里做耗时操作。如果高优先级中断来了,你的低优先级中断会被“打断”。我曾经在UART中断里做浮点运算,结果定时器中断丢了——血的教训。

4.3 外部中断配置——按键、传感器信号处理

外部中断,就是通过引脚触发的。MSP430的P1和P2口都支持外部中断。

配置步骤其实就三步:

  1. 设置引脚方向为输入——PxDIR &= ~BITx
  2. 选择中断触发边沿——上升沿还是下降沿
  3. 使能中断——PxIE |= BITx

来看个实际例子。我做一个低功耗按键唤醒项目时,这样配置的:

void Key_Init(void)
{
    // P1.3作为按键输入
    P1DIR &= ~BIT3;      // 输入方向
    P1REN |= BIT3;       // 使能上拉电阻
    P1OUT |= BIT3;       // 上拉
    
    // 配置下降沿触发
    P1IES |= BIT3;       // 下降沿触发
    
    // 清除中断标志
    P1IFG &= ~BIT3;
    
    // 使能中断
    P1IE |= BIT3;
    
    // 总中断使能
    __enable_interrupt();
}

// 中断服务函数
#pragma vector = PORT1_VECTOR
__interrupt void Port1_ISR(void)
{
    if(P1IFG & BIT3)
    {
        // 按键被按下
        // 这里加20ms延时消抖
        __delay_cycles(20000);
        if(!(P1IN & BIT3))
        {
            // 确认按键按下
            Key_Action();
        }
        P1IFG &= ~BIT3;  // 清除标志
    }
}

注意:退出中断前一定要清除中断标志!我见过有人忘了清标志,结果中断反复触发,系统直接卡死。

4.4 定时器中断——精准的时间控制

定时器中断是嵌入式开发里最常用的。MSP430的Timer_A和Timer_B都支持多种模式。

我个人最常用的是“增计数模式”。说白了就是计数器从0加到设定值,然后触发中断,再回到0重新加。

配置定时器中断的步骤:

  1. 选择时钟源——SMCLK、ACLK还是TACLK
  2. 设置分频系数——ID0、ID1位控制
  3. 设定计数模式——增计数、连续计数还是增减计数
  4. 写入比较值——CCR0决定周期
  5. 使能中断——CCIE位

举个例子,我要产生1ms的定时中断:

void TimerA_Init(void)
{
    // 假设SMCLK = 1MHz
    // 1ms = 1000个时钟周期
    
    TA0CTL = TASSEL_2 + ID_0 + MC_1;  // SMCLK, 不分频, 增计数
    TA0CCR0 = 1000 - 1;                // 1ms定时
    TA0CCTL0 = CCIE;                   // 使能比较中断
    
    __enable_interrupt();
}

#pragma vector = TIMERA0_VECTOR
__interrupt void Timer_A0_ISR(void)
{
    // 每1ms执行一次
    static uint16_t ms_counter = 0;
    ms_counter++;
    
    if(ms_counter >= 1000)
    {
        ms_counter = 0;
        // 1秒到了,做点什么
        LED_Toggle();
    }
}

避坑指南:我曾经把CCR0设成1000,以为就是1ms。结果忘了考虑时钟频率——SMCLK实际是1.048576MHz。算下来差了将近5%。所以一定要先确认时钟频率,再算比较值。

4.5 中断嵌套与注意事项

MSP430默认不支持中断嵌套。也就是说,一个中断正在处理时,其他中断进不来。

如果想实现中断嵌套,得在中断服务函数里手动开总中断:

#pragma vector = TIMERA0_VECTOR
__interrupt void Timer_A0_ISR(void)
{
    __bis_SR_register(GIE);  // 手动开中断,允许嵌套
    
    // 这里可以被打断
    // 处理耗时任务
    
    // 退出时自动恢复SR
}

警告:中断嵌套要慎用!我见过一个项目,开了嵌套后,低优先级中断被高优先级中断反复打断,堆栈直接溢出。除非你很清楚自己在做什么,否则别开嵌套。

最后总结几个要点:

  • 中断服务函数要短小精悍,别在里面做复杂运算
  • 全局变量如果被中断和主循环共用,记得加volatile关键字
  • 退出中断前一定要清标志位
  • 调试时可以用LED翻转来观察中断是否正常触发

中断系统是MSP430的“灵魂”。用好了,你的程序就能高效响应各种事件。用不好,那就是一场灾难。希望今天的分享能帮你少走弯路。