跳转至

中断安全与临界区保护

概述

在嵌入式系统中,中断服务程序(ISR)和主程序经常需要访问相同的数据和资源。如果不采取适当的保护措施,就会出现数据不一致、竞态条件等严重问题。本教程将深入讲解中断安全编程的核心概念和实践技巧。

完成本教程后,你将能够:

  • 理解中断环境下的数据安全问题
  • 掌握临界区的概念和识别方法
  • 学会使用多种临界区保护机制
  • 理解原子操作的实现原理
  • 掌握避免竞态条件的编程技巧
  • 学会分析和解决数据一致性问题

学习目标

  1. 深入理解竞态条件和数据不一致的根本原因
  2. 掌握临界区的识别和保护方法
  3. 学会使用PRIMASK、BASEPRI进行中断屏蔽
  4. 理解原子操作的实现和应用
  5. 掌握volatile关键字的正确使用
  6. 学会设计中断安全的数据结构
  7. 掌握调试中断安全问题的技巧

前置要求

知识要求

  • 理解中断的基本概念和工作流程
  • 掌握中断优先级配置方法
  • 了解C语言指针和内存操作
  • 理解多任务并发的基本概念

硬件要求

  • STM32开发板(F1/F4/F7系列均可)
  • LED灯(用于状态指示)
  • 按键(用于触发中断)
  • USB转串口模块(用于调试输出)
  • 逻辑分析仪(可选,用于时序分析)

软件要求

  • STM32CubeIDE或Keil MDK
  • STM32 HAL库
  • 串口终端软件

背景知识

什么是竞态条件?

竞态条件(Race Condition)是指多个执行流(主程序和ISR)访问共享资源时,最终结果取决于执行的时序顺序,导致不可预测的行为。

经典示例

// 全局变量
volatile uint32_t counter = 0;

// 主程序
void main_function(void)
{
    counter++;  // 非原子操作!
}

// 中断服务程序
void IRQHandler(void)
{
    counter++;  // 非原子操作!
}

问题分析

counter++ 实际上是三个步骤:

; counter++ 的汇编实现
LDR  R0, [counter]    ; 1. 读取counter到寄存器
ADD  R0, R0, #1       ; 2. 寄存器值加1
STR  R0, [counter]    ; 3. 写回counter

; 如果在步骤1和步骤3之间发生中断:
; 主程序: LDR R0, [counter]  ; R0 = 5
; 主程序: ADD R0, R0, #1     ; R0 = 6
; [中断发生]
; ISR: LDR R0, [counter]     ; R0 = 5 (旧值!)
; ISR: ADD R0, R0, #1        ; R0 = 6
; ISR: STR R0, [counter]     ; counter = 6
; [中断返回]
; 主程序: STR R0, [counter]  ; counter = 6 (覆盖了ISR的结果!)
; 
; 结果:counter应该是7,实际是6,丢失了一次增量!

什么是临界区?

临界区(Critical Section)是指访问共享资源的代码段,在执行期间不能被中断打断,以保证操作的原子性。

临界区的特征: - 访问共享数据或资源 - 需要保证操作的完整性 - 执行期间不能被打断 - 应该尽可能短

什么是原子操作?

原子操作(Atomic Operation)是指不可分割的操作,要么完全执行,要么完全不执行,不会被中断打断。

核心内容

1. 识别中断安全问题

1.1 常见的不安全模式

模式1:非原子的读-改-写操作

// ❌ 不安全:counter++不是原子操作
volatile uint32_t counter = 0;

void main_loop(void)
{
    counter++;  // 可能被中断打断
}

void TIM_IRQHandler(void)
{
    counter++;  // 可能导致数据丢失
}

模式2:多字节数据的非原子访问

// ❌ 不安全:32位数据的读写不是原子的
volatile uint32_t timestamp = 0;

void main_loop(void)
{
    // 读取可能被中断打断
    uint32_t time = timestamp;
    process_time(time);
}

void TIM_IRQHandler(void)
{
    // 写入可能在主程序读取过程中发生
    timestamp = HAL_GetTick();
}

模式3:结构体的非原子访问

// ❌ 不安全:结构体访问不是原子的
typedef struct {
    uint16_t x;
    uint16_t y;
    uint8_t  status;
} SensorData_t;

volatile SensorData_t sensor_data;

void main_loop(void)
{
    // 读取过程可能被中断
    SensorData_t local_data = sensor_data;
    process_data(&local_data);
}

void ADC_IRQHandler(void)
{
    // 写入可能在主程序读取过程中发生
    sensor_data.x = read_adc_x();
    sensor_data.y = read_adc_y();
    sensor_data.status = 1;
}

模式4:标志位的非原子操作

// ❌ 不安全:位操作不是原子的
volatile uint32_t flags = 0;

#define FLAG_UART   (1 << 0)
#define FLAG_TIMER  (1 << 1)
#define FLAG_ADC    (1 << 2)

void main_loop(void)
{
    // 读-改-写操作可能被中断
    flags |= FLAG_UART;
}

void TIM_IRQHandler(void)
{
    // 可能覆盖主程序的修改
    flags |= FLAG_TIMER;
}

1.2 识别临界区的方法

检查清单

  1. 是否访问共享变量?
  2. 全局变量
  3. 静态变量
  4. 通过指针访问的数据

  5. 是否被多个执行流访问?

  6. 主程序和ISR
  7. 多个ISR
  8. 主程序的不同位置

  9. 操作是否可分割?

  10. 多条指令组成
  11. 读-改-写操作
  12. 多字节数据访问

  13. 中断是否可能发生?

  14. 在操作过程中
  15. 在任意指令之间

示例分析

// 分析以下代码的临界区
volatile uint32_t rx_count = 0;
volatile uint8_t rx_buffer[256];
volatile uint8_t rx_index = 0;

void UART_IRQHandler(void)
{
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE)) {
        // 临界区1:修改rx_buffer和rx_index
        rx_buffer[rx_index++] = huart1.Instance->DR;
        rx_count++;  // 临界区2:修改rx_count
    }
}

void main_loop(void)
{
    // 临界区3:读取rx_count
    if (rx_count > 0) {
        // 临界区4:修改rx_count
        rx_count--;

        // 临界区5:读取rx_buffer和rx_index
        uint8_t data = rx_buffer[0];

        // 移动数据(需要保护)
        for (int i = 0; i < rx_index - 1; i++) {
            rx_buffer[i] = rx_buffer[i + 1];
        }
        rx_index--;
    }
}

// 问题:
// 1. rx_count的读写不是原子的
// 2. rx_buffer和rx_index的访问可能被中断
// 3. 数据移动过程可能被中断,导致数据不一致

2. 临界区保护方法

2.1 使用PRIMASK屏蔽所有中断

PRIMASK是ARM Cortex-M的特殊寄存器,可以屏蔽所有可屏蔽中断。

基本用法

/**
 * @brief  使用PRIMASK保护临界区
 * @note   屏蔽所有可屏蔽中断
 */
void critical_section_primask(void)
{
    // 禁止中断
    __disable_irq();  // 设置PRIMASK = 1

    // 临界区代码
    shared_variable++;

    // 恢复中断
    __enable_irq();   // 清除PRIMASK = 0
}

保存和恢复中断状态

/**
 * @brief  保存和恢复中断状态
 * @note   支持嵌套调用
 */
void critical_section_with_save(void)
{
    // 保存当前中断状态
    uint32_t primask = __get_PRIMASK();

    // 禁止中断
    __disable_irq();

    // 临界区代码
    shared_variable++;

    // 恢复之前的中断状态
    __set_PRIMASK(primask);
}

宏定义封装

// 定义临界区保护宏
#define ENTER_CRITICAL()  uint32_t primask_save = __get_PRIMASK(); \
                          __disable_irq()

#define EXIT_CRITICAL()   __set_PRIMASK(primask_save)

// 使用示例
void protected_function(void)
{
    ENTER_CRITICAL();

    // 临界区代码
    counter++;
    buffer[index++] = data;

    EXIT_CRITICAL();
}

优点: - 实现简单 - 保护完全 - 开销小(几个时钟周期)

缺点: - 屏蔽所有中断,影响实时性 - 不适合长临界区 - 可能导致中断丢失

适用场景: - 临界区非常短(<10μs) - 需要完全保护 - 不需要响应任何中断

2.2 使用BASEPRI选择性屏蔽中断

BASEPRI寄存器可以屏蔽优先级低于某个值的中断,保留高优先级中断的响应能力。

基本用法

/**
 * @brief  使用BASEPRI保护临界区
 * @note   只屏蔽低优先级中断
 * @param  priority: 优先级阈值(0-255)
 */
void critical_section_basepri(uint32_t priority)
{
    // 保存当前BASEPRI
    uint32_t basepri_save = __get_BASEPRI();

    // 设置优先级阈值(屏蔽优先级 >= priority的中断)
    __set_BASEPRI(priority);

    // 临界区代码
    shared_variable++;

    // 恢复BASEPRI
    __set_BASEPRI(basepri_save);
}

实际应用示例

// 优先级配置(分组2)
#define PRIORITY_EMERGENCY   0x00  // 抢占0,响应0
#define PRIORITY_CONTROL     0x10  // 抢占1,响应0
#define PRIORITY_COMM        0x20  // 抢占2,响应0
#define PRIORITY_BACKGROUND  0x30  // 抢占3,响应0

/**
 * @brief  保护临界区,但保留紧急中断
 */
void protected_operation(void)
{
    uint32_t basepri_save = __get_BASEPRI();

    // 屏蔽优先级1-3的中断,保留优先级0(紧急中断)
    __set_BASEPRI(0x10);

    // 临界区代码
    // 紧急中断仍然可以响应
    update_shared_data();

    __set_BASEPRI(basepri_save);
}

宏定义封装

// 定义BASEPRI保护宏
#define ENTER_CRITICAL_BASEPRI(pri)  \
    uint32_t basepri_save = __get_BASEPRI(); \
    __set_BASEPRI(pri)

#define EXIT_CRITICAL_BASEPRI()  \
    __set_BASEPRI(basepri_save)

// 使用示例
void communication_handler(void)
{
    // 屏蔽通信和后台中断,保留紧急和控制中断
    ENTER_CRITICAL_BASEPRI(0x20);

    // 临界区代码
    process_communication_data();

    EXIT_CRITICAL_BASEPRI();
}

优点: - 可以保留高优先级中断 - 更好的实时性 - 灵活的保护级别

缺点: - 需要正确配置优先级 - 比PRIMASK稍复杂 - 需要理解优先级系统

适用场景: - 需要保留高优先级中断 - 临界区较长(10-100μs) - 有明确的优先级层次

2.3 禁用特定中断

对于只需要保护特定中断的场景,可以只禁用相关的中断源。

基本用法

/**
 * @brief  禁用特定中断保护临界区
 */
void critical_section_specific_irq(void)
{
    // 禁用UART中断
    HAL_NVIC_DisableIRQ(USART1_IRQn);

    // 临界区代码
    // 只保护与UART相关的数据
    uart_buffer[uart_index++] = data;

    // 恢复UART中断
    HAL_NVIC_EnableIRQ(USART1_IRQn);
}

多中断保护

/**
 * @brief  禁用多个相关中断
 */
void critical_section_multiple_irq(void)
{
    // 禁用相关中断
    HAL_NVIC_DisableIRQ(USART1_IRQn);
    HAL_NVIC_DisableIRQ(TIM2_IRQn);
    HAL_NVIC_DisableIRQ(DMA1_Stream5_IRQn);

    // 临界区代码
    process_shared_data();

    // 恢复中断
    HAL_NVIC_EnableIRQ(USART1_IRQn);
    HAL_NVIC_EnableIRQ(TIM2_IRQn);
    HAL_NVIC_EnableIRQ(DMA1_Stream5_IRQn);
}

优点: - 只影响相关中断 - 其他中断正常响应 - 最小化对实时性的影响

缺点: - 需要明确知道哪些中断访问共享资源 - 代码较复杂 - 容易遗漏

适用场景: - 明确知道数据访问关系 - 需要最小化对其他中断的影响 - 临界区较长

3. 原子操作

3.1 硬件原子指令

ARM Cortex-M提供了专门的原子指令:LDREX和STREX。

LDREX/STREX原理

/**
 * @brief  使用LDREX/STREX实现原子加法
 * @param  addr: 变量地址
 * @param  value: 要加的值
 * @retval 操作后的值
 */
uint32_t atomic_add(volatile uint32_t *addr, uint32_t value)
{
    uint32_t old_value, new_value, status;

    do {
        // LDREX: 独占读取
        old_value = __LDREXW(addr);

        // 计算新值
        new_value = old_value + value;

        // STREX: 独占写入
        // 返回0表示成功,返回1表示失败(需要重试)
        status = __STREXW(new_value, addr);

    } while (status != 0);  // 失败则重试

    return new_value;
}

原子操作函数库

/**
 * @brief  原子递增
 */
uint32_t atomic_increment(volatile uint32_t *addr)
{
    uint32_t old_value, new_value, status;

    do {
        old_value = __LDREXW(addr);
        new_value = old_value + 1;
        status = __STREXW(new_value, addr);
    } while (status != 0);

    return new_value;
}

/**
 * @brief  原子递减
 */
uint32_t atomic_decrement(volatile uint32_t *addr)
{
    uint32_t old_value, new_value, status;

    do {
        old_value = __LDREXW(addr);
        new_value = old_value - 1;
        status = __STREXW(new_value, addr);
    } while (status != 0);

    return new_value;
}

/**
 * @brief  原子比较并交换
 * @param  addr: 变量地址
 * @param  expected: 期望值
 * @param  desired: 新值
 * @retval 1表示成功,0表示失败
 */
uint32_t atomic_compare_exchange(volatile uint32_t *addr,
                                  uint32_t expected,
                                  uint32_t desired)
{
    uint32_t old_value, status;

    old_value = __LDREXW(addr);

    if (old_value == expected) {
        status = __STREXW(desired, addr);
        return (status == 0) ? 1 : 0;
    } else {
        __CLREX();  // 清除独占标记
        return 0;
    }
}

使用示例

// 全局计数器
volatile uint32_t event_counter = 0;

// 主程序
void main_loop(void)
{
    // 原子递增,不需要禁用中断
    atomic_increment(&event_counter);
}

// 中断服务程序
void TIM_IRQHandler(void)
{
    // 原子递增,不会与主程序冲突
    atomic_increment(&event_counter);
}

3.2 单字节和单字操作的原子性

在ARM Cortex-M上,某些操作天然是原子的:

原子操作

// ✅ 原子操作(单条指令)
volatile uint8_t  byte_var = 0;
volatile uint16_t half_var = 0;
volatile uint32_t word_var = 0;

// 单字节读写是原子的
byte_var = 1;           // 原子
uint8_t b = byte_var;   // 原子

// 对齐的半字读写是原子的
half_var = 100;         // 原子(如果对齐)
uint16_t h = half_var;  // 原子(如果对齐)

// 对齐的字读写是原子的
word_var = 1000;        // 原子(如果对齐)
uint32_t w = word_var;  // 原子(如果对齐)

非原子操作

// ❌ 非原子操作(多条指令)
volatile uint32_t counter = 0;

counter++;              // 非原子(读-改-写)
counter += 5;           // 非原子(读-改-写)
counter = counter * 2;  // 非原子(读-改-写)

// 位操作也不是原子的
volatile uint32_t flags = 0;
flags |= (1 << 3);      // 非原子(读-改-写)
flags &= ~(1 << 5);     // 非原子(读-改-写)

安全的标志位操作

// 方法1:使用独立的字节标志
volatile uint8_t flag_uart = 0;
volatile uint8_t flag_timer = 0;
volatile uint8_t flag_adc = 0;

// 每个标志独立,写入是原子的
void UART_IRQHandler(void)
{
    flag_uart = 1;  // 原子操作
}

void main_loop(void)
{
    if (flag_uart) {
        flag_uart = 0;  // 原子操作
        handle_uart();
    }
}

// 方法2:使用原子位操作(如果硬件支持)
volatile uint32_t flags = 0;

void set_flag_atomic(uint32_t flag_bit)
{
    uint32_t old_val, new_val, status;

    do {
        old_val = __LDREXW(&flags);
        new_val = old_val | (1 << flag_bit);
        status = __STREXW(new_val, &flags);
    } while (status != 0);
}

void clear_flag_atomic(uint32_t flag_bit)
{
    uint32_t old_val, new_val, status;

    do {
        old_val = __LDREXW(&flags);
        new_val = old_val & ~(1 << flag_bit);
        status = __STREXW(new_val, &flags);
    } while (status != 0);
}

4. volatile关键字

4.1 volatile的作用

volatile关键字告诉编译器,变量的值可能在程序控制之外被改变(如被ISR修改),防止编译器优化。

没有volatile的问题

// ❌ 错误:没有volatile
uint32_t flag = 0;

void main_loop(void)
{
    while (flag == 0) {
        // 编译器可能优化为:
        // if (flag == 0) while(1);
        // 因为编译器认为flag不会改变
    }

    process_data();
}

void IRQHandler(void)
{
    flag = 1;  // 主程序可能看不到这个改变!
}

使用volatile

// ✅ 正确:使用volatile
volatile uint32_t flag = 0;

void main_loop(void)
{
    while (flag == 0) {
        // 编译器每次都会从内存读取flag
        // 不会优化掉这个检查
    }

    process_data();
}

void IRQHandler(void)
{
    flag = 1;  // 主程序能正确看到改变
}

4.2 volatile的正确使用

规则1:ISR和主程序共享的变量必须是volatile

// ✅ 正确
volatile uint8_t rx_flag = 0;
volatile uint32_t rx_count = 0;
volatile uint8_t rx_buffer[256];

void UART_IRQHandler(void)
{
    rx_buffer[rx_count++] = read_uart();
    rx_flag = 1;
}

规则2:volatile不保证原子性

// ❌ 错误:volatile不能保证原子性
volatile uint32_t counter = 0;

void main_loop(void)
{
    counter++;  // 仍然不是原子操作!
}

void IRQHandler(void)
{
    counter++;  // 可能导致数据丢失!
}

// ✅ 正确:需要额外的保护
volatile uint32_t counter = 0;

void main_loop(void)
{
    __disable_irq();
    counter++;
    __enable_irq();
}

规则3:指针和指向的数据都可能需要volatile

// 不同的volatile用法
volatile uint32_t *ptr1;        // 指针可变,数据不可变
uint32_t * volatile ptr2;       // 指针不可变,数据可变
volatile uint32_t * volatile ptr3;  // 指针和数据都可变

// 实际应用
volatile uint8_t *rx_buffer_ptr;  // 指向接收缓冲区的指针

5. 中断安全的数据结构

5.1 环形缓冲区(Ring Buffer)

环形缓冲区是中断安全的经典数据结构,适用于生产者-消费者模式。

基本实现

/**
 * @brief  环形缓冲区结构
 */
typedef struct {
    volatile uint8_t  buffer[256];
    volatile uint16_t head;  // 写入位置(生产者)
    volatile uint16_t tail;  // 读取位置(消费者)
    uint16_t size;           // 缓冲区大小
} RingBuffer_t;

/**
 * @brief  初始化环形缓冲区
 */
void RingBuffer_Init(RingBuffer_t *rb)
{
    rb->head = 0;
    rb->tail = 0;
    rb->size = 256;
}

/**
 * @brief  写入数据(在ISR中调用)
 * @retval 1表示成功,0表示缓冲区满
 */
uint8_t RingBuffer_Put(RingBuffer_t *rb, uint8_t data)
{
    uint16_t next_head = (rb->head + 1) % rb->size;

    // 检查缓冲区是否满
    if (next_head == rb->tail) {
        return 0;  // 缓冲区满
    }

    // 写入数据
    rb->buffer[rb->head] = data;

    // 更新head(原子操作)
    rb->head = next_head;

    return 1;
}

/**
 * @brief  读取数据(在主程序中调用)
 * @retval 1表示成功,0表示缓冲区空
 */
uint8_t RingBuffer_Get(RingBuffer_t *rb, uint8_t *data)
{
    // 检查缓冲区是否空
    if (rb->head == rb->tail) {
        return 0;  // 缓冲区空
    }

    // 读取数据
    *data = rb->buffer[rb->tail];

    // 更新tail(原子操作)
    rb->tail = (rb->tail + 1) % rb->size;

    return 1;
}

/**
 * @brief  获取缓冲区中的数据数量
 */
uint16_t RingBuffer_Count(RingBuffer_t *rb)
{
    // 读取head和tail(可能不一致,但安全)
    uint16_t h = rb->head;
    uint16_t t = rb->tail;

    if (h >= t) {
        return h - t;
    } else {
        return rb->size - t + h;
    }
}

使用示例

// 全局环形缓冲区
RingBuffer_t uart_rx_buffer;

// 初始化
void UART_Init(void)
{
    RingBuffer_Init(&uart_rx_buffer);
    // ... 其他初始化
}

// ISR中写入
void UART_IRQHandler(void)
{
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE)) {
        uint8_t data = huart1.Instance->DR;

        if (!RingBuffer_Put(&uart_rx_buffer, data)) {
            // 缓冲区满,数据丢失
            error_count++;
        }
    }
}

// 主程序中读取
void main_loop(void)
{
    uint8_t data;

    while (RingBuffer_Get(&uart_rx_buffer, &data)) {
        // 处理接收到的数据
        process_uart_data(data);
    }
}

为什么环形缓冲区是中断安全的?

  1. 单生产者单消费者:ISR只写head,主程序只写tail
  2. 原子更新:head和tail的更新是单字写入,是原子的
  3. 无竞争:读写操作不会相互干扰

5.2 双缓冲技术

双缓冲技术使用两个缓冲区,一个用于写入,一个用于读取,通过切换实现无锁访问。

基本实现

/**
 * @brief  双缓冲结构
 */
typedef struct {
    uint16_t x;
    uint16_t y;
    uint16_t z;
} SensorData_t;

// 两个缓冲区
SensorData_t buffer[2];

// 当前写入缓冲区索引(ISR修改)
volatile uint8_t write_index = 0;

// 当前读取缓冲区索引(主程序修改)
volatile uint8_t read_index = 1;

// 数据就绪标志
volatile uint8_t data_ready = 0;

/**
 * @brief  ISR中写入数据
 */
void ADC_IRQHandler(void)
{
    // 写入当前缓冲区
    buffer[write_index].x = read_adc_x();
    buffer[write_index].y = read_adc_y();
    buffer[write_index].z = read_adc_z();

    // 切换缓冲区(原子操作)
    write_index = 1 - write_index;

    // 设置数据就绪标志
    data_ready = 1;
}

/**
 * @brief  主程序中读取数据
 */
void main_loop(void)
{
    if (data_ready) {
        data_ready = 0;

        // 读取数据(不会被ISR干扰)
        SensorData_t local_data = buffer[read_index];

        // 切换读取缓冲区
        read_index = 1 - read_index;

        // 处理数据
        process_sensor_data(&local_data);
    }
}

优点: - 无需禁用中断 - 读写完全独立 - 适合大数据块传输

缺点: - 需要双倍内存 - 只适合单生产者单消费者 - 可能有一帧延迟

5.3 标志位数组

使用独立的标志位避免位操作的竞态条件。

/**
 * @brief  使用独立标志位
 */
typedef struct {
    volatile uint8_t uart_rx;
    volatile uint8_t timer_tick;
    volatile uint8_t adc_complete;
    volatile uint8_t button_pressed;
} EventFlags_t;

EventFlags_t event_flags = {0};

// ISR中设置标志(原子操作)
void UART_IRQHandler(void)
{
    event_flags.uart_rx = 1;
}

void TIM_IRQHandler(void)
{
    event_flags.timer_tick = 1;
}

// 主程序中检查和清除标志
void main_loop(void)
{
    // 检查UART标志
    if (event_flags.uart_rx) {
        event_flags.uart_rx = 0;  // 原子清除
        handle_uart();
    }

    // 检查定时器标志
    if (event_flags.timer_tick) {
        event_flags.timer_tick = 0;
        handle_timer();
    }
}

6. 实践项目

项目:中断安全的数据采集系统

实现一个完整的数据采集系统,包含多个中断源和共享数据,演示各种保护技术。

项目需求

  1. ADC采集(定时器触发)
  2. 每10ms采集一次
  3. 采集3个通道
  4. 数据存入环形缓冲区

  5. 串口通信(中断接收)

  6. 接收命令
  7. 发送采集数据
  8. 使用环形缓冲区

  9. 按键输入(外部中断)

  10. 启动/停止采集
  11. 清除数据
  12. 需要防抖

  13. 状态显示(定时器中断)

  14. 每秒更新一次
  15. 显示采集状态和数据量

完整代码实现

#include "stm32f4xx_hal.h"
#include <stdio.h>
#include <string.h>

// ==================== 环形缓冲区 ====================

typedef struct {
    volatile uint8_t buffer[256];
    volatile uint16_t head;
    volatile uint16_t tail;
    uint16_t size;
} RingBuffer_t;

void RingBuffer_Init(RingBuffer_t *rb)
{
    rb->head = 0;
    rb->tail = 0;
    rb->size = 256;
}

uint8_t RingBuffer_Put(RingBuffer_t *rb, uint8_t data)
{
    uint16_t next_head = (rb->head + 1) % rb->size;

    if (next_head == rb->tail) {
        return 0;
    }

    rb->buffer[rb->head] = data;
    rb->head = next_head;

    return 1;
}

uint8_t RingBuffer_Get(RingBuffer_t *rb, uint8_t *data)
{
    if (rb->head == rb->tail) {
        return 0;
    }

    *data = rb->buffer[rb->tail];
    rb->tail = (rb->tail + 1) % rb->size;

    return 1;
}

uint16_t RingBuffer_Count(RingBuffer_t *rb)
{
    uint16_t h = rb->head;
    uint16_t t = rb->tail;

    return (h >= t) ? (h - t) : (rb->size - t + h);
}

// ==================== ADC数据结构 ====================

typedef struct { uint16_t ch0; uint16_t ch1; uint16_t ch2; uint32_t timestamp; } ADC_Sample_t;

// ADC数据缓冲区

define ADC_BUFFER_SIZE 100

ADC_Sample_t adc_buffer[ADC_BUFFER_SIZE]; volatile uint16_t adc_write_index = 0; volatile uint16_t adc_read_index = 0;

// ==================== 系统状态 ====================

typedef enum { STATE_IDLE, STATE_SAMPLING, STATE_PAUSED } SystemState_t;

volatile SystemState_t system_state = STATE_IDLE;

// ==================== 统计信息 ====================

typedef struct { volatile uint32_t adc_samples; volatile uint32_t uart_rx_bytes; volatile uint32_t uart_tx_bytes; volatile uint32_t button_presses; volatile uint32_t buffer_overflows; } Statistics_t;

Statistics_t stats = {0};

// ==================== 串口缓冲区 ====================

RingBuffer_t uart_rx_buffer; RingBuffer_t uart_tx_buffer;

// ==================== 临界区保护宏 ====================

#define ENTER_CRITICAL() uint32_t primask_save = __get_PRIMASK(); \ __disable_irq()

define EXIT_CRITICAL() __set_PRIMASK(primask_save)

// ==================== ADC采集(定时器触发)====================

void TIM2_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE)) { __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);

    // 只在采样状态下采集
    if (system_state == STATE_SAMPLING) {
        // 计算下一个写入位置
        uint16_t next_index = (adc_write_index + 1) % ADC_BUFFER_SIZE;

        // 检查缓冲区是否满
        if (next_index == adc_read_index) {
            // 缓冲区满,记录溢出
            stats.buffer_overflows++;
            return;
        }

        // 读取ADC数据
        adc_buffer[adc_write_index].ch0 = HAL_ADC_GetValue(&hadc1);
        adc_buffer[adc_write_index].ch1 = HAL_ADC_GetValue(&hadc2);
        adc_buffer[adc_write_index].ch2 = HAL_ADC_GetValue(&hadc3);
        adc_buffer[adc_write_index].timestamp = HAL_GetTick();

        // 更新写入索引(原子操作)
        adc_write_index = next_index;

        // 更新统计
        stats.adc_samples++;
    }
}

}

// ==================== 串口接收中断 ====================

void USART1_IRQHandler(void) { if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE)) { uint8_t data = huart1.Instance->DR;

    // 写入环形缓冲区
    if (RingBuffer_Put(&uart_rx_buffer, data)) {
        stats.uart_rx_bytes++;
    }
}

// 发送中断
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TXE)) {
    uint8_t data;

    if (RingBuffer_Get(&uart_tx_buffer, &data)) {
        huart1.Instance->DR = data;
        stats.uart_tx_bytes++;
    } else {
        // 缓冲区空,禁用发送中断
        __HAL_UART_DISABLE_IT(&huart1, UART_IT_TXE);
    }
}

}

// ==================== 按键中断 ====================

volatile uint32_t button_last_time = 0;

void EXTI0_IRQHandler(void) { if (__HAL_GPIO_EXTI_GET_IT(BUTTON_PIN)) { __HAL_GPIO_EXTI_CLEAR_IT(BUTTON_PIN);

    uint32_t current_time = HAL_GetTick();

    // 软件防抖(50ms)
    if (current_time - button_last_time > 50) {
        button_last_time = current_time;

        // 切换状态(使用临界区保护)
        ENTER_CRITICAL();

        if (system_state == STATE_IDLE) {
            system_state = STATE_SAMPLING;
        } else if (system_state == STATE_SAMPLING) {
            system_state = STATE_PAUSED;
        } else {
            system_state = STATE_IDLE;
        }

        EXIT_CRITICAL();

        stats.button_presses++;
    }
}

}

// ==================== 状态显示(定时器中断)====================

void TIM3_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(&htim3, TIM_FLAG_UPDATE)) { __HAL_TIM_CLEAR_FLAG(&htim3, TIM_FLAG_UPDATE);

    // 读取统计信息(使用临界区保护)
    ENTER_CRITICAL();

    Statistics_t local_stats = stats;
    SystemState_t local_state = system_state;
    uint16_t adc_count = (adc_write_index >= adc_read_index) ?
                         (adc_write_index - adc_read_index) :
                         (ADC_BUFFER_SIZE - adc_read_index + adc_write_index);

    EXIT_CRITICAL();

    // 打印状态(在临界区外)
    printf("\n=== System Status ===\n");
    printf("State: %s\n",
           local_state == STATE_IDLE ? "IDLE" :
           local_state == STATE_SAMPLING ? "SAMPLING" : "PAUSED");
    printf("ADC Samples: %lu\n", local_stats.adc_samples);
    printf("Buffer Count: %u\n", adc_count);
    printf("UART RX: %lu bytes\n", local_stats.uart_rx_bytes);
    printf("UART TX: %lu bytes\n", local_stats.uart_tx_bytes);
    printf("Button Presses: %lu\n", local_stats.button_presses);
    printf("Buffer Overflows: %lu\n", local_stats.buffer_overflows);
    printf("====================\n\n");
}

} // ==================== 主程序 ==================== /** * @brief 处理串口命令 */ void process_uart_command(void) { uint8_t cmd; while (RingBuffer_Get(&uart_rx_buffer, &cmd)) { switch (cmd) { case 'S': // Start sampling case 's': ENTER_CRITICAL(); system_state = STATE_SAMPLING; EXIT_CRITICAL(); printf("Sampling started\n"); break; case 'P': // Pause sampling case 'p': ENTER_CRITICAL(); system_state = STATE_PAUSED; EXIT_CRITICAL(); printf("Sampling paused\n"); break; case 'C': // Clear data case 'c': ENTER_CRITICAL(); adc_read_index = adc_write_index; memset(&stats, 0, sizeof(stats)); EXIT_CRITICAL(); printf("Data cleared\n"); break; case 'D': // Dump data case 'd': dump_adc_data(); break; default: printf("Unknown command: %c\n", cmd); break; } } } /** * @brief 导出ADC数据 */ void dump_adc_data(void) { printf("\n=== ADC Data Dump ===\n"); // 读取当前索引(原子操作) uint16_t write_idx = adc_write_index; uint16_t read_idx = adc_read_index; uint16_t count = 0; while (read_idx != write_idx && count < 10) { ADC_Sample_t sample; // 读取样本(使用临界区保护) ENTER_CRITICAL(); sample = adc_buffer[read_idx]; read_idx = (read_idx + 1) % ADC_BUFFER_SIZE; EXIT_CRITICAL(); // 打印数据 printf("[%lu] CH0=%u, CH1=%u, CH2=%u\n", sample.timestamp, sample.ch0, sample.ch1, sample.ch2); count++; } // 更新读取索引 ENTER_CRITICAL(); adc_read_index = read_idx; EXIT_CRITICAL(); printf("===================\n\n"); } /** * @brief 主函数 */ int main(void) { HAL_Init(); SystemClock_Config(); // 初始化硬件 GPIO_Init(); UART_Init(); ADC_Init(); Timer_Init(); // 初始化缓冲区 RingBuffer_Init(&uart_rx_buffer); RingBuffer_Init(&uart_tx_buffer); // 配置中断优先级 HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2); // ADC采集:抢占1,响应0 HAL_NVIC_SetPriority(TIM2_IRQn, 1, 0); HAL_NVIC_EnableIRQ(TIM2_IRQn); // 串口通信:抢占1,响应1 HAL_NVIC_SetPriority(USART1_IRQn, 1, 1); HAL_NVIC_EnableIRQ(USART1_IRQn); // 按键输入:抢占0,响应0(最高优先级) HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0); HAL_NVIC_EnableIRQ(EXTI0_IRQn); // 状态显示:抢占2,响应0(最低优先级) HAL_NVIC_SetPriority(TIM3_IRQn, 2, 0); HAL_NVIC_EnableIRQ(TIM3_IRQn); printf("\n=== Interrupt-Safe Data Acquisition System ===\n"); printf("Commands:\n"); printf(" S - Start sampling\n"); printf(" P - Pause sampling\n"); printf(" C - Clear data\n"); printf(" D - Dump data\n"); printf("==============================================\n\n"); // 启动定时器 HAL_TIM_Base_Start_IT(&htim2); // ADC采集定时器 HAL_TIM_Base_Start_IT(&htim3); // 状态显示定时器 while (1) { // 处理串口命令 process_uart_command(); // 处理ADC数据(如果需要) // ... // 空闲时可以进入低功耗模式 // HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI); } }

项目特点

  1. 多种保护技术
  2. 环形缓冲区:串口和ADC数据
  3. 临界区保护:状态切换和统计信息
  4. 原子操作:索引更新

  5. 中断优先级设计

  6. 按键:最高优先级(紧急响应)
  7. ADC和串口:中等优先级(实时数据)
  8. 状态显示:最低优先级(后台任务)

  9. 数据一致性保证

  10. 读写分离
  11. 原子更新
  12. 临界区保护

  13. 性能优化

  14. ISR快速返回
  15. 主循环处理复杂逻辑
  16. 最小化临界区

调试技巧

1. 检测竞态条件

方法1:使用断言检查数据一致性

#include <assert.h>

// 检查环形缓冲区的一致性
void RingBuffer_Check(RingBuffer_t *rb)
{
    uint16_t h = rb->head;
    uint16_t t = rb->tail;

    // head和tail必须在有效范围内
    assert(h < rb->size);
    assert(t < rb->size);

    // 计数不能超过缓冲区大小
    uint16_t count = (h >= t) ? (h - t) : (rb->size - t + h);
    assert(count < rb->size);
}

// 在关键位置调用检查
void main_loop(void)
{
    RingBuffer_Check(&uart_rx_buffer);

    // 处理数据
    process_data();
}

方法2:使用GPIO标记临界区

#define DEBUG_PIN  GPIO_PIN_8

// 进入临界区时拉高GPIO
#define ENTER_CRITICAL_DEBUG()  \
    HAL_GPIO_WritePin(GPIOD, DEBUG_PIN, GPIO_PIN_SET); \
    uint32_t primask_save = __get_PRIMASK(); \
    __disable_irq()

// 退出临界区时拉低GPIO
#define EXIT_CRITICAL_DEBUG()  \
    __set_PRIMASK(primask_save); \
    HAL_GPIO_WritePin(GPIOD, DEBUG_PIN, GPIO_PIN_RESET)

// 使用逻辑分析仪观察:
// - 临界区的持续时间
// - 临界区的频率
// - 是否有嵌套

方法3:添加计数器检测

// 检测数据丢失
volatile uint32_t tx_sequence = 0;
volatile uint32_t rx_sequence = 0;

void UART_TX_IRQHandler(void)
{
    // 发送序列号
    send_data(tx_sequence++);
}

void UART_RX_IRQHandler(void)
{
    uint32_t received_seq = receive_data();

    // 检查序列号连续性
    if (received_seq != rx_sequence) {
        printf("Data loss detected! Expected %lu, got %lu\n",
               rx_sequence, received_seq);
    }

    rx_sequence = received_seq + 1;
}

2. 分析临界区性能

测量临界区持续时间

// 使用DWT计数器测量
void measure_critical_section(void)
{
    // 启用DWT
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;

    uint32_t start = DWT->CYCCNT;

    ENTER_CRITICAL();

    // 临界区代码
    critical_operation();

    EXIT_CRITICAL();

    uint32_t end = DWT->CYCCNT;
    uint32_t cycles = end - start;

    // 转换为微秒(假设72MHz)
    float us = (float)cycles / 72.0f;

    printf("Critical section: %lu cycles (%.2f us)\n", cycles, us);
}

统计临界区使用情况

typedef struct {
    uint32_t enter_count;
    uint32_t total_cycles;
    uint32_t max_cycles;
    uint32_t min_cycles;
} CriticalStats_t;

CriticalStats_t crit_stats = {
    .min_cycles = 0xFFFFFFFF
};

#define ENTER_CRITICAL_STATS()  \
    uint32_t _start = DWT->CYCCNT; \
    uint32_t primask_save = __get_PRIMASK(); \
    __disable_irq()

#define EXIT_CRITICAL_STATS()  \
    __set_PRIMASK(primask_save); \
    uint32_t _end = DWT->CYCCNT; \
    uint32_t _cycles = _end - _start; \
    crit_stats.enter_count++; \
    crit_stats.total_cycles += _cycles; \
    if (_cycles > crit_stats.max_cycles) \
        crit_stats.max_cycles = _cycles; \
    if (_cycles < crit_stats.min_cycles) \
        crit_stats.min_cycles = _cycles

// 打印统计信息
void print_critical_stats(void)
{
    float avg = (float)crit_stats.total_cycles / crit_stats.enter_count;

    printf("Critical Section Statistics:\n");
    printf("  Count: %lu\n", crit_stats.enter_count);
    printf("  Average: %.2f cycles\n", avg);
    printf("  Max: %lu cycles\n", crit_stats.max_cycles);
    printf("  Min: %lu cycles\n", crit_stats.min_cycles);
}

3. 常见问题排查

问题1:数据偶尔出错

// 可能原因:竞态条件
// 排查方法:

// 1. 检查是否使用volatile
volatile uint32_t shared_var;  // 必须有volatile

// 2. 检查是否有临界区保护
ENTER_CRITICAL();
shared_var++;  // 必须在临界区内
EXIT_CRITICAL();

// 3. 检查多字节数据的原子性
typedef struct {
    uint32_t data1;
    uint32_t data2;
} Data_t;

volatile Data_t shared_data;

// ❌ 错误:结构体赋值不是原子的
shared_data = local_data;

// ✅ 正确:使用临界区保护
ENTER_CRITICAL();
shared_data = local_data;
EXIT_CRITICAL();

问题2:系统偶尔死锁

// 可能原因:中断中等待资源
// 排查方法:

// ❌ 错误:在ISR中等待
void IRQHandler(void)
{
    while (busy_flag) {
        // 死锁!如果busy_flag在主程序中设置
    }
}

// ✅ 正确:使用标志位
void IRQHandler(void)
{
    if (!busy_flag) {
        data_ready = 1;
    }
}

问题3:中断响应延迟

// 可能原因:临界区过长
// 排查方法:

// ❌ 错误:临界区太长
ENTER_CRITICAL();
for (int i = 0; i < 10000; i++) {
    process_data(i);  // 长时间处理
}
EXIT_CRITICAL();

// ✅ 正确:缩短临界区
ENTER_CRITICAL();
Data_t local_data = shared_data;  // 快速复制
EXIT_CRITICAL();

// 在临界区外处理
for (int i = 0; i < 10000; i++) {
    process_data(i);
}

深入理解

内存屏障和缓存一致性

在某些高级处理器上,编译器和CPU可能会重排指令以优化性能,这可能导致中断安全问题。

内存屏障的作用

// 内存屏障确保指令顺序
__DMB();  // Data Memory Barrier - 数据内存屏障
__DSB();  // Data Synchronization Barrier - 数据同步屏障
__ISB();  // Instruction Synchronization Barrier - 指令同步屏障

// 示例:确保写入顺序
void set_flag_with_barrier(void)
{
    data_ready = 1;
    __DMB();  // 确保data_ready写入完成
    flag = 1;  // 然后才写入flag
}

volatile的局限性

// volatile只保证不优化,不保证顺序
volatile uint32_t data = 0;
volatile uint32_t flag = 0;

// 编译器可能重排这两条语句
data = 123;
flag = 1;

// 使用内存屏障确保顺序
data = 123;
__DMB();
flag = 1;

原子操作的实现原理

LDREX/STREX的工作机制

1. LDREX读取内存,并设置独占监视器
2. 执行计算
3. STREX尝试写入:
   - 如果独占监视器仍然有效 → 写入成功,返回0
   - 如果独占监视器被清除 → 写入失败,返回1

独占监视器会在以下情况被清除:
- 其他处理器访问了相同地址
- 发生了上下文切换
- 执行了CLREX指令

为什么需要循环重试

uint32_t atomic_add(volatile uint32_t *addr, uint32_t value)
{
    uint32_t old_value, new_value, status;

    do {
        old_value = __LDREXW(addr);  // 独占读取
        new_value = old_value + value;
        status = __STREXW(new_value, addr);  // 独占写入

        // 如果在读取和写入之间发生中断,
        // 中断可能修改了相同地址,
        // STREX会失败,需要重试

    } while (status != 0);

    return new_value;
}

单生产者单消费者的无锁设计

环形缓冲区之所以是无锁的,关键在于:

1. 单生产者单消费者模型
   - ISR只写head
   - 主程序只写tail
   - 没有写冲突

2. 读写分离
   - 生产者读tail,写head
   - 消费者读head,写tail
   - 读操作不需要保护

3. 原子更新
   - head和tail是单字变量
   - 单字读写是原子的
   - 不需要额外保护

4. 内存顺序
   - 先写数据,后更新索引
   - 确保数据可见性

为什么多生产者或多消费者需要锁

// 多生产者场景
void Producer1_IRQHandler(void)
{
    // 读取head
    uint16_t next = (head + 1) % size;

    // [可能被Producer2打断]

    // 写入数据
    buffer[head] = data1;

    // 更新head(可能覆盖Producer2的更新)
    head = next;
}

void Producer2_IRQHandler(void)
{
    // 同样的操作,可能导致数据丢失
    uint16_t next = (head + 1) % size;
    buffer[head] = data2;
    head = next;
}

// 解决方案:使用原子操作或锁

最佳实践

1. 设计原则

原则1:最小化临界区

// ❌ 不好:临界区太大
ENTER_CRITICAL();
read_sensor_data();      // 耗时操作
process_data();          // 耗时操作
update_display();        // 耗时操作
EXIT_CRITICAL();

// ✅ 好:只保护必要的部分
read_sensor_data();      // 在临界区外

ENTER_CRITICAL();
shared_data = sensor_data;  // 只保护共享数据访问
EXIT_CRITICAL();

process_data();          // 在临界区外
update_display();        // 在临界区外

原则2:避免在ISR中等待

// ❌ 不好:在ISR中等待
void IRQHandler(void)
{
    while (busy) {
        // 等待,阻塞其他中断
    }

    process_data();
}

// ✅ 好:使用标志位
void IRQHandler(void)
{
    if (!busy) {
        data_ready = 1;
    }
}

void main_loop(void)
{
    if (data_ready && !busy) {
        data_ready = 0;
        process_data();
    }
}

原则3:使用合适的数据结构

// ✅ 好:使用环形缓冲区
RingBuffer_t buffer;

void ISR(void)
{
    RingBuffer_Put(&buffer, data);  // 无锁操作
}

void main(void)
{
    uint8_t data;
    while (RingBuffer_Get(&buffer, &data)) {
        process(data);
    }
}

2. 代码审查清单

共享数据检查: - [ ] 所有共享变量都声明为volatile - [ ] 多字节数据的访问有临界区保护 - [ ] 结构体的访问有临界区保护 - [ ] 位操作有临界区保护或使用原子操作

临界区检查: - [ ] 临界区尽可能短 - [ ] 没有在临界区内调用耗时函数 - [ ] 没有在临界区内等待事件 - [ ] 正确保存和恢复中断状态

ISR检查: - [ ] ISR尽可能短 - [ ] 没有在ISR中等待 - [ ] 没有在ISR中调用阻塞函数 - [ ] 正确清除中断标志

数据结构检查: - [ ] 使用合适的无锁数据结构 - [ ] 环形缓冲区的索引更新是原子的 - [ ] 双缓冲的切换是原子的

3. 性能优化

优化1:使用原子操作代替临界区

// 方案A:使用临界区(开销较大)
ENTER_CRITICAL();
counter++;
EXIT_CRITICAL();

// 方案B:使用原子操作(开销较小)
atomic_increment(&counter);

优化2:批量处理减少临界区次数

// ❌ 不好:频繁进出临界区
for (int i = 0; i < 100; i++) {
    ENTER_CRITICAL();
    buffer[i] = data[i];
    EXIT_CRITICAL();
}

// ✅ 好:一次性处理
ENTER_CRITICAL();
memcpy(buffer, data, 100);
EXIT_CRITICAL();

优化3:使用BASEPRI代替PRIMASK

// 方案A:PRIMASK(屏蔽所有中断)
ENTER_CRITICAL();
update_data();
EXIT_CRITICAL();

// 方案B:BASEPRI(保留高优先级中断)
ENTER_CRITICAL_BASEPRI(0x20);
update_data();
EXIT_CRITICAL_BASEPRI();

常见问题

Q1: volatile和临界区保护有什么区别?

A: 它们解决不同的问题:

volatile的作用: - 防止编译器优化 - 确保每次都从内存读取 - 不保证原子性 - 不保证指令顺序

临界区保护的作用: - 保证操作的原子性 - 防止中断打断 - 保护多步骤操作 - 确保数据一致性

示例

// 只用volatile - 不安全
volatile uint32_t counter = 0;

void main(void)
{
    counter++;  // 仍然不是原子操作!
}

void ISR(void)
{
    counter++;  // 可能导致数据丢失
}

// volatile + 临界区 - 安全
volatile uint32_t counter = 0;

void main(void)
{
    ENTER_CRITICAL();
    counter++;  // 原子操作
    EXIT_CRITICAL();
}

void ISR(void)
{
    counter++;  // ISR中不需要保护(不会被打断)
}

结论: - volatile是必要的,但不充分 - 需要同时使用volatile和临界区保护 - volatile确保可见性,临界区确保原子性

Q2: 什么时候需要使用临界区保护?

A: 以下情况需要临界区保护:

1. 非原子的读-改-写操作

// 需要保护
counter++;
flags |= FLAG_BIT;
array[index++] = data;

2. 多字节数据的访问

// 需要保护
uint32_t timestamp = shared_timestamp;
shared_data = local_data;

3. 结构体的访问

// 需要保护
SensorData_t data = shared_sensor_data;
shared_config = new_config;

4. 多个相关变量的更新

// 需要保护(保证一致性)
buffer[index] = data;
index++;
count++;

不需要保护的情况

// 单字节的读写(如果对齐)
volatile uint8_t flag = 1;

// 只读操作(如果数据不会被修改)
uint32_t value = read_only_data;

// ISR中的操作(不会被更高优先级打断)
void ISR(void)
{
    counter++;  // 如果没有更高优先级ISR访问counter
}

Q3: PRIMASK和BASEPRI应该如何选择?

A: 根据需求选择:

使用PRIMASK的场景: - 临界区非常短(<10μs) - 需要完全保护 - 不需要响应任何中断

ENTER_CRITICAL();  // 使用PRIMASK
shared_var++;      // 极短的操作
EXIT_CRITICAL();

使用BASEPRI的场景: - 临界区较长(10-100μs) - 需要保留高优先级中断 - 有明确的优先级层次

ENTER_CRITICAL_BASEPRI(0x20);  // 保留优先级0-1
process_data();                 // 较长的操作
EXIT_CRITICAL_BASEPRI();

对比表

特性 PRIMASK BASEPRI
屏蔽范围 所有可屏蔽中断 选择性屏蔽
实时性 影响所有中断 保留高优先级
使用场景 短临界区 长临界区
复杂度 简单 稍复杂
开销 最小 稍大

Q4: 环形缓冲区为什么是中断安全的?

A: 环形缓冲区的中断安全性基于以下设计:

1. 单生产者单消费者模型

// ISR只写head
void ISR(void)
{
    buffer[head] = data;
    head = (head + 1) % size;  // 只修改head
}

// 主程序只写tail
void main(void)
{
    data = buffer[tail];
    tail = (tail + 1) % size;  // 只修改tail
}

// 没有写冲突!

2. 原子更新

// head和tail是单字变量
volatile uint16_t head;
volatile uint16_t tail;

// 单字写入是原子的
head = new_head;  // 原子操作
tail = new_tail;  // 原子操作

3. 读写分离

// 生产者读tail(判断是否满),写head
uint16_t next = (head + 1) % size;
if (next != tail) {  // 读tail
    buffer[head] = data;
    head = next;     // 写head
}

// 消费者读head(判断是否空),写tail
if (head != tail) {  // 读head
    data = buffer[tail];
    tail = (tail + 1) % size;  // 写tail
}

// 读操作不需要保护!

4. 内存顺序保证

// 先写数据,后更新索引
buffer[head] = data;  // 1. 写数据
head = next_head;     // 2. 更新索引

// 消费者看到新的head时,数据一定已经写入

注意: - 只适用于单生产者单消费者 - 多生产者或多消费者需要额外保护 - 缓冲区大小通常是2的幂次方(优化取模运算)

Q5: 如何调试中断安全问题?

A: 使用以下方法:

1. 添加断言检查

#include <assert.h>

// 检查数据一致性
void check_consistency(void)
{
    assert(head < size);
    assert(tail < size);
    assert(count <= size);
}

2. 使用GPIO标记

// 进入临界区时拉高GPIO
ENTER_CRITICAL();
HAL_GPIO_WritePin(DEBUG_PORT, DEBUG_PIN, GPIO_PIN_SET);

// 临界区代码
critical_operation();

// 退出临界区时拉低GPIO
HAL_GPIO_WritePin(DEBUG_PORT, DEBUG_PIN, GPIO_PIN_RESET);
EXIT_CRITICAL();

// 使用逻辑分析仪观察GPIO波形

3. 添加统计计数器

volatile uint32_t error_count = 0;
volatile uint32_t overflow_count = 0;

void check_and_count(void)
{
    if (data_inconsistent()) {
        error_count++;
    }

    if (buffer_full()) {
        overflow_count++;
    }
}

4. 使用内存监视

// 在调试器中设置数据断点
// 当共享变量被修改时暂停
// 查看调用栈确定修改位置

5. 压力测试

// 增加中断频率
// 增加数据量
// 长时间运行
// 观察是否出现错误

总结

本教程深入讲解了中断安全编程的核心概念和实践技巧,核心要点包括:

  1. 竞态条件和数据不一致
  2. 多个执行流访问共享资源
  3. 非原子操作导致数据丢失
  4. 需要识别和保护临界区

  5. 临界区保护方法

  6. PRIMASK:屏蔽所有中断
  7. BASEPRI:选择性屏蔽
  8. 禁用特定中断
  9. 选择合适的方法

  10. 原子操作

  11. LDREX/STREX硬件指令
  12. 单字节和单字操作的原子性
  13. 原子操作库的实现

  14. volatile关键字

  15. 防止编译器优化
  16. 不保证原子性
  17. 必须与临界区保护配合使用

  18. 中断安全的数据结构

  19. 环形缓冲区
  20. 双缓冲技术
  21. 标志位数组
  22. 无锁设计原则

  23. 最佳实践

  24. 最小化临界区
  25. 避免在ISR中等待
  26. 使用合适的数据结构
  27. 正确使用volatile

掌握这些知识后,你就可以编写出安全、可靠的中断程序,避免竞态条件和数据不一致问题。

延伸阅读

参考资料

  1. ARM Cortex-M Programming Guide to Memory Barrier Instructions
  2. "Embedded Systems Architecture" by Tammy Noergaard
  3. "Real-Time Systems Design and Analysis" by Phillip A. Laplante
  4. ARM Cortex-M4 Technical Reference Manual

练习题

  1. 分析以下代码的中断安全问题,并给出修正方案:
volatile uint32_t buffer[10];
volatile uint8_t index = 0;

void main(void)
{
    for (int i = 0; i < 10; i++) {
        buffer[index++] = read_data();
    }
}

void ISR(void)
{
    index = 0;
}
  1. 实现一个中断安全的计数器,支持原子递增、递减和读取操作。

  2. 设计一个双缓冲系统,用于ADC数据采集,要求:

  3. ISR写入一个缓冲区
  4. 主程序读取另一个缓冲区
  5. 自动切换缓冲区

  6. 实现一个环形缓冲区,支持批量写入和批量读取操作。

  7. 分析你的项目中的共享数据,识别所有需要保护的临界区。

下一步:建议学习 中断性能优化与延迟分析,了解如何优化中断系统的性能。