1070 字
5 分钟
异步编程之旅(01)裸机中的同步异步

最早让我对这些异步编程产生兴趣的是 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 应该是快速退出的。


异步编程之旅(01)裸机中的同步异步
https://hyrsoft.github.io/posts/异步编程之旅/裸机中的同步异步/
作者
好软好温暖
发布于
2026-02-14
许可协议
CC BY-NC-SA 4.0