最早让我对这些异步编程产生兴趣的是 Rust 的 Tokio 异步运行时和 Embassy 框架,其中 Embassy 框架在单片机里引入了无栈协程,这对于传统 RTOS 和基于回调的事件驱动来说无疑是革命性的,让我对其产生了浓厚兴趣。
异步编程需要的前置概念实在太多,牵扯到
- 阻塞IO/非阻塞IO
- 同步/异步
- select/poll
- epoll
- io_uring
- 协程
以及很多Linux系统编程和操作系统的概念,作为一个电子工程的学生,我还是决定先从最早熟悉的单片机裸机开始,一步步梳理一下我对异步编程的理解。
一、裸机框架中的同步
单片机裸机,可以视为一个单线程的应用,在 main 函数开始执行后,总体是一个同步逻辑:
// 伪代码void main(void) { // 1. 系统初始化(时钟、GPIO、外设等) clock_init(); // 初始化系统时钟 gpio_init(); // 初始化 GPIO 管脚 uart_init(115200); // 初始化串口通信 timer_init(); // 初始化定时器 dma_init(); // 初始化 DMA 控制器
// 2. 启用全局中断 enable_global_interrupt();
// 3. 主循环:不断执行业务逻辑 while (1) { // 执行具体的业务逻辑 // 处理传感器数据、通信、计算等 do_business_logic();
}}在最基础的裸机代码中,一切都是顺序执行的(Synchronous)。 当我们需要处理一个 IO 密集型任务(如等待传感器数据、DMA 搬运、串口发送)时,最简单的逻辑就是轮询(Polling)。
以 DMA 数据传输为例,裸机中确认 DMA 传输是否完成,可以通过轮循的方式:
CPU 发起 DMA 传输请求后,进入一个死循环,不断查询状态寄存器。
// 伪代码void dma_transfer_polling(uint32_t src, uint32_t dst, uint32_t len) { // 1. 配置硬件:源地址、目的地址、长度 DMA_REG->SAR = src; DMA_REG->DAR = dst; DMA_REG->CNT = len;
// 2. 启动 DMA DMA_REG->CR |= DMA_CR_EN;
// 3. [阻塞点]:死循环检查标志位 // CPU 100% 耗在此处,无法响应其他任务(除非是高优先级中断) while (!(DMA_REG->SR & DMA_SR_TCIF)) { // CPU 只是在不断读取总线上的寄存器状态 __NOP(); }
// 4. 清除标志位,关闭 DMA DMA_REG->SR &= ~DMA_SR_TCIF; DMA_REG->CR &= ~DMA_CR_EN;}这里的操作就是同步的,由于裸机框架中,没有操作系统的任务调度,CPU 只能卡在这里,直到 DMA 传输完成,函数才会返回,才能继续执行后续代码。
这里的 CPU 实际上是一个忙等状态,不断对 DMA 状态寄存器进行轮询(Poll)。
二、裸机框架中的异步
中断(Interrupt)是裸机框架中唯一的、真正的异步机制。 它允许 CPU 在发起硬件请求后,立即返回去执行 while(1) 循环中的其他逻辑,直到硬件完成任务,并通过中断服务函数(ISR),来让 CPU 执行后续逻辑。
CPU 完成 DMA 配置后,就去负责其他事情,当 DMA 传输完成后,通过硬件中断,CPU 进入中断服务函数,来完成相关操作:
// 伪代码// 全局状态标志(需加 volatile 防止编译器过度优化)volatile bool g_dma_done = false;
void dma_transfer_irq(uint32_t src, uint32_t dst, uint32_t len) { // 1. 配置并开启 DMA 中断使能 (TCIE) DMA_REG->CR |= DMA_CR_TCIE;
// 2. 设置地址与长度并启动 DMA_REG->SAR = src; DMA_REG->DAR = dst; DMA_REG->CNT = len; DMA_REG->CR |= DMA_CR_EN;
// 3. [非阻塞]:直接返回,CPU 可以去跑 main 循环里的其他逻辑 return;}
// 硬件自动调用的中断服务程序 (ISR)void DMA_IRQHandler(void) { if (DMA_REG->SR & DMA_SR_TCIF) { // 处理业务逻辑(例如设置标志位或调用回调函数) g_dma_done = true;
// 关键:清除中断标志,否则会死循环进入中断 DMA_REG->SR &= ~DMA_SR_TCIF; DMA_REG->CR &= ~DMA_CR_EN; }}在调用dma_transfer_irq后,函数立即返回,去执行while(1)循环后面的任务,当 DMA 传输完成后,通过中断强制切换 CPU 来运行 ISR,在里面执行业务逻辑。
但其实一般不会在 ISR 中执行耗时的业务逻辑,一般是设置标志位,在while(1)中读取改标志位,如果已经被设置,则执行耗时的业务逻辑。ISR 应该是快速退出的。