中断安全与临界区保护¶
概述¶
在嵌入式系统中,中断服务程序(ISR)和主程序经常需要访问相同的数据和资源。如果不采取适当的保护措施,就会出现数据不一致、竞态条件等严重问题。本教程将深入讲解中断安全编程的核心概念和实践技巧。
完成本教程后,你将能够:
- 理解中断环境下的数据安全问题
- 掌握临界区的概念和识别方法
- 学会使用多种临界区保护机制
- 理解原子操作的实现原理
- 掌握避免竞态条件的编程技巧
- 学会分析和解决数据一致性问题
学习目标¶
- 深入理解竞态条件和数据不一致的根本原因
- 掌握临界区的识别和保护方法
- 学会使用PRIMASK、BASEPRI进行中断屏蔽
- 理解原子操作的实现和应用
- 掌握volatile关键字的正确使用
- 学会设计中断安全的数据结构
- 掌握调试中断安全问题的技巧
前置要求¶
知识要求¶
- 理解中断的基本概念和工作流程
- 掌握中断优先级配置方法
- 了解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 识别临界区的方法¶
检查清单:
- 是否访问共享变量?
- 全局变量
- 静态变量
-
通过指针访问的数据
-
是否被多个执行流访问?
- 主程序和ISR
- 多个ISR
-
主程序的不同位置
-
操作是否可分割?
- 多条指令组成
- 读-改-写操作
-
多字节数据访问
-
中断是否可能发生?
- 在操作过程中
- 在任意指令之间
示例分析:
// 分析以下代码的临界区
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);
}
}
为什么环形缓冲区是中断安全的?
- 单生产者单消费者:ISR只写head,主程序只写tail
- 原子更新:head和tail的更新是单字写入,是原子的
- 无竞争:读写操作不会相互干扰
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. 实践项目¶
项目:中断安全的数据采集系统¶
实现一个完整的数据采集系统,包含多个中断源和共享数据,演示各种保护技术。
项目需求:
- ADC采集(定时器触发)
- 每10ms采集一次
- 采集3个通道
-
数据存入环形缓冲区
-
串口通信(中断接收)
- 接收命令
- 发送采集数据
-
使用环形缓冲区
-
按键输入(外部中断)
- 启动/停止采集
- 清除数据
-
需要防抖
-
状态显示(定时器中断)
- 每秒更新一次
- 显示采集状态和数据量
完整代码实现:
#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);
}
}
项目特点:
- 多种保护技术
- 环形缓冲区:串口和ADC数据
- 临界区保护:状态切换和统计信息
-
原子操作:索引更新
-
中断优先级设计
- 按键:最高优先级(紧急响应)
- ADC和串口:中等优先级(实时数据)
-
状态显示:最低优先级(后台任务)
-
数据一致性保证
- 读写分离
- 原子更新
-
临界区保护
-
性能优化
- ISR快速返回
- 主循环处理复杂逻辑
- 最小化临界区
调试技巧¶
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. 非原子的读-改-写操作
2. 多字节数据的访问
3. 结构体的访问
4. 多个相关变量的更新
不需要保护的情况:
// 单字节的读写(如果对齐)
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) - 需要完全保护 - 不需要响应任何中断
使用BASEPRI的场景: - 临界区较长(10-100μs) - 需要保留高优先级中断 - 有明确的优先级层次
对比表:
| 特性 | 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. 内存顺序保证
注意: - 只适用于单生产者单消费者 - 多生产者或多消费者需要额外保护 - 缓冲区大小通常是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. 压力测试
总结¶
本教程深入讲解了中断安全编程的核心概念和实践技巧,核心要点包括:
- 竞态条件和数据不一致
- 多个执行流访问共享资源
- 非原子操作导致数据丢失
-
需要识别和保护临界区
-
临界区保护方法
- PRIMASK:屏蔽所有中断
- BASEPRI:选择性屏蔽
- 禁用特定中断
-
选择合适的方法
-
原子操作
- LDREX/STREX硬件指令
- 单字节和单字操作的原子性
-
原子操作库的实现
-
volatile关键字
- 防止编译器优化
- 不保证原子性
-
必须与临界区保护配合使用
-
中断安全的数据结构
- 环形缓冲区
- 双缓冲技术
- 标志位数组
-
无锁设计原则
-
最佳实践
- 最小化临界区
- 避免在ISR中等待
- 使用合适的数据结构
- 正确使用volatile
掌握这些知识后,你就可以编写出安全、可靠的中断程序,避免竞态条件和数据不一致问题。
延伸阅读¶
- 中断优先级配置与抢占机制 - 理解优先级管理
- 中断与轮询的选择策略 - 选择合适的机制
- 中断性能优化与延迟分析 - 优化中断性能
参考资料¶
- ARM Cortex-M Programming Guide to Memory Barrier Instructions
- "Embedded Systems Architecture" by Tammy Noergaard
- "Real-Time Systems Design and Analysis" by Phillip A. Laplante
- ARM Cortex-M4 Technical Reference Manual
练习题:
- 分析以下代码的中断安全问题,并给出修正方案:
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;
}
-
实现一个中断安全的计数器,支持原子递增、递减和读取操作。
-
设计一个双缓冲系统,用于ADC数据采集,要求:
- ISR写入一个缓冲区
- 主程序读取另一个缓冲区
-
自动切换缓冲区
-
实现一个环形缓冲区,支持批量写入和批量读取操作。
-
分析你的项目中的共享数据,识别所有需要保护的临界区。
下一步:建议学习 中断性能优化与延迟分析,了解如何优化中断系统的性能。