在嵌入式系统开发中,尤其是使用 STM32 微控制器时,非阻塞编程是一种非常重要的编程方式。它能够提高系统的响应性、实时性和效率,使得系统在处理多个任务时更加灵活和可靠。
一、非阻塞编程的概念
非阻塞编程与传统的阻塞编程方式相对。在阻塞编程中,当一个函数或操作执行时,程序会一直等待该操作完成,在此期间无法进行其他任务。例如,当进行串口数据发送时,如果使用阻塞方式,程序会一直停留在发送函数处,直到数据全部发送完毕,这期间无法处理其他事件,可能会导致系统对其他重要事件的响应延迟。
而非阻塞编程则允许程序在执行某个操作时,不必等待该操作立即完成,可以继续执行其他任务。当该操作完成时,系统会通过某种方式通知程序。这样可以充分利用处理器的时间,提高系统的整体性能和并发处理能力。
二、STM32 中非阻塞编程的实现方式
(一)中断驱动
- 原理
- STM32 具有丰富的中断资源。通过将外设的事件(如串口接收数据、定时器溢出等)配置为中断源,当事件发生时,微控制器会暂停当前正在执行的任务,转而执行相应的中断服务程序(ISR)。在 ISR 中处理事件相关的操作,然后快速返回原来的任务继续执行。这样就实现了在不阻塞主线程的情况下对事件的及时响应。
- 示例 - 串口接收非阻塞
- 首先,需要配置串口相关的寄存器和中断。假设使用 USART1 进行串口通信。
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE)!= RESET)
{
// 读取接收到的数据
uint8_t data = USART_ReceiveData(USART1);
// 将数据存储到缓冲区或进行其他处理
// 这里只是简单示例,可以根据实际需求修改
received_data_buffer[received_data_index++] = data;
if (received_data_index >= BUFFER_SIZE)
{
received_data_index = 0;
}
// 清除接收中断标志
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
- 在主函数中,进行串口和中断的初始化:
int main(void)
{
// 初始化串口
USART_InitTypeDef USART_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
USART_InitStructure.USART_BaudRate = 9600;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
// 开启串口接收中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
// 启动串口
USART_Cmd(USART1, ENABLE);
while (1)
{
// 这里可以执行其他任务,而不会因为串口接收而阻塞
// 例如处理其他外设数据、进行计算等
}
}
(二)状态机
- 原理
- 状态机是一种通过定义不同的状态和状态之间的转换来实现非阻塞编程的方法。对于一个需要按步骤执行的操作,将其分解为多个状态,程序根据当前状态执行相应的操作,并根据操作的结果或外部事件决定是否转换到下一个状态。这样可以在每个循环中检查状态并执行相应的任务片段,而不是一次性等待整个操作完成。
- 示例 - 简单的状态机实现数据传输
- 假设要实现一个从传感器读取数据并通过串口发送的操作,分为读取数据、处理数据和发送数据三个状态。
typedef enum
{
STATE_READ_DATA,
STATE_PROCESS_DATA,
STATE_SEND_DATA
} State_t;
State_t current_state = STATE_READ_DATA;
void process_data_task(void)
{
switch (current_state)
{
case STATE_READ_DATA:
// 从传感器读取数据
read_sensor_data();
// 如果数据读取成功,转换到处理数据状态
if (data_read_success)
{
current_state = STATE_PROCESS_DATA;
}
break;
case STATE_PROCESS_DATA:
// 对读取的数据进行处理
process_sensor_data();
// 处理完成后,转换到发送数据状态
current_state = STATE_SEND_DATA;
break;
case STATE_SEND_DATA:
// 将处理后的数据通过串口发送
send_data_through_uart();
// 发送完成后,回到读取数据状态准备下一次循环
current_state = STATE_READ_DATA;
break;
}
}
- 在主函数中,可以周期性地调用
process_data_task
函数:
int main(void)
{
while (1)
{
process_data_task();
// 可以同时执行其他不相关的任务
}
}
(三)定时器与轮询结合
- 原理
- 使用 STM32 的定时器来控制操作的时间间隔。通过设置定时器的周期,定期触发中断或在主循环中进行轮询检查。在定时器触发时或轮询到指定时间时,执行相应的任务片段,而不是一直等待任务完成。这种方式可以用于需要定时执行某些操作或者对时间有严格要求的场景。
- 示例 - 定时采集数据并处理
- 首先,配置定时器相关的寄存器。假设使用 TIM2 作为定时器。
void timer_init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
TIM_TimeBaseStructure.TIM_Period = 1000 - 1; // 设置定时器周期为1000,即定时1ms(根据实际需求修改)
TIM_TimeBaseStructure.TIM_Prescaler = 7200 - 1; // 根据系统时钟设置预分频值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // 开启定时器更新中断
TIM_Cmd(TIM2, ENABLE);
}
- 定时器中断服务程序:
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update)!= RESET)
{
// 执行数据采集和处理任务
collect_and_process_data();
// 清除中断标志
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
- 在主函数中,除了初始化定时器外,还可以进行其他任务的处理:
int main(void)
{
timer_init();
while (1)
{
// 这里可以执行其他与数据采集处理不冲突的任务
// 例如显示数据、与其他外设通信等
}
}
三、非阻塞编程的优势
(一)提高系统响应性
在多任务环境下,非阻塞编程能够确保系统及时响应各种事件,不会因为某个长时间运行的任务而忽略其他重要事件。例如,在一个同时需要处理串口通信、按键输入和数据采集的系统中,如果串口发送采用阻塞方式,当发送大量数据时,可能会导致按键输入无法及时响应,而使用非阻塞方式则可以避免这种情况。
(二)增强系统实时性
对于实时性要求较高的系统,非阻塞编程可以更好地满足时间约束。通过及时处理关键事件和任务,系统能够在规定的时间内完成相应的操作,提高系统的可靠性和稳定性。例如在工业控制系统中,需要及时响应传感器的变化并进行相应的控制操作,非阻塞编程可以确保这种实时性要求得到满足。
(三)优化资源利用
非阻塞编程可以充分利用处理器的空闲时间,提高资源利用率。当一个任务不需要立即等待结果时,处理器可以切换去执行其他任务,而不是处于空闲等待状态。这样可以在相同的硬件资源下处理更多的任务,提高系统的整体性能。
(四)便于代码维护和扩展
非阻塞编程通常将复杂的操作分解为多个小的、可管理的任务片段,每个片段对应一个状态或一个特定的操作。这种模块化的编程方式使得代码更易于理解、维护和扩展。当需要添加新的功能或修改现有功能时,只需要对相应的状态或任务片段进行修改,而不会影响整个系统的结构和逻辑。
四、注意事项
(一)数据一致性和同步问题
在非阻塞编程中,由于多个任务可能同时访问和修改共享数据,因此需要特别注意数据一致性和同步问题。可以使用互斥锁、信号量等机制来保护共享数据,确保在多任务环境下数据的正确性。例如,在串口接收数据并存储到缓冲区的过程中,如果同时有其他任务也在读取或修改该缓冲区,就可能导致数据错误,此时可以使用互斥锁来保证在同一时间只有一个任务能够访问缓冲区。
(二)任务优先级和调度
合理设置任务的优先级对于非阻塞编程非常重要。关键任务应该具有较高的优先级,以确保能够及时得到处理。同时,需要考虑任务的调度策略,避免高优先级任务长时间占用处理器资源,导致低优先级任务无法执行。STM32 的操作系统(如 FreeRTOS 等)提供了丰富的任务调度机制,可以根据实际需求进行配置和使用。
(三)中断处理的合理性
中断服务程序应该尽量短小精悍,只进行关键的处理操作,避免在中断中执行耗时过长的任务,以免影响系统的实时性和其他中断的响应。如果中断处理过于复杂,可以将一部分任务放到主程序中或使用任务队列等方式进行后续处理,确保中断能够及时返回,不影响其他中断的触发和处理。
(四)内存管理
非阻塞编程中可能会频繁地创建和销毁任务、缓冲区等对象,需要注意合理的内存管理,避免内存泄漏和碎片问题。可以使用动态内存分配函数(如malloc
和free
)时要谨慎,确保及时释放不再使用的内存。对于一些频繁使用的固定大小的缓冲区等,可以采用静态内存分配的方式,以提高内存使用效率和减少内存管理的复杂性。