MicroPython+Pico 接收空闲中断和发送空闲中断全实战

1.实验前的准备

在下面实验中,我们需要将通过 MiniUSB 数据线将 风雅一号板-通用兼容扩展板UART0USB 接口与电脑上的 USB 接口连接:

2.接收空闲中断(IRQ_RXIDLE):接收完整数据帧

串口接收空闲中断(IRQ_RXIDLE)是 MicroPython / 嵌入式开发中针对串口通信的核心中断机制,通俗来说:当串口接收端(RX)从 “有数据持续传输” 的忙碌状态,进入 “指定时间内无新字节接收” 的空闲状态时,就会触发该中断。它的核心价值是精准识别 “一帧完整的数据 / 指令” 接收完成 —— 不用依赖固定帧长、结束符(如 \r\n),只要数据传输暂停(空闲),就判定一帧数据接收完毕,完美解决了 “如何判断不定长指令传输结束” 的问题,是物联网、工业控制等场景中解析串口指令帧的常用方案。

这里,我们在下面的代码中周期性采集模拟温湿度数据,通过串口空闲中断响应特定指令并返回对应数据;串口接收到指令触发空闲中断后,解析指令并返回环境数据(或未知指令提示),随后重置缓冲区。

完整代码如下:

# Python env   : MicroPython v1.27.0
# -*- coding: utf-8 -*-        
# @Time    : 2025/12/28 上午10:37   
# @Author  : 李清水            
# @File    : main.py       
# @Description : 串口接收空闲中断(IRQ_RXIDLE),接收完整指令并返回环境数据

# ======================================== 导入相关模块 =========================================

# 导入MicroPython标准库模块
from machine import Pin, UART
# 导入时间模块
import time
# 用于模拟环境传感器数据
import random

# ======================================== 全局变量 ============================================

# 环境数据缓存(温度+湿度)
env_data_cache = {"temp": 0.0, "humi": 0.0}
# 串口接收缓冲区(预分配128字节)
rx_buffer = bytearray(128)
# 缓冲区索引
buf_index = 0

# ======================================== 功能函数 ============================================

def uart_rxidle_handler(uart: UART) -> None:
    """
​    串口接收空闲中断处理函数:读取完整指令并返回环境数据。

​    Args:
​        uart (UART): 触发中断的UART实例(machine.UART类型)。

​    Notes:
​        - 中断触发时读取所有未处理字符至缓冲区,避免数据丢失。
​        - 缓冲区最大长度限制为64字节,防止索引越界导致程序异常。
​        - 仅支持UTF-8编码指令解析,非UTF-8字符可能导致解码失败。
​        - 识别"GET_ENV_DATA"指令并返回温湿度数据,其他指令返回未知提示。
​        - 指令解析完成后立即重置缓冲区索引,确保下一次接收不受影响。
​    """
​    ​global buf_index, env_data_cache

    # 读取所有未处理的字符到缓冲区
    while uart.any() and buf_index < len(rx_buffer):
        rx_buffer[buf_index] = uart.read(1)[0]
        buf_index += 1

    # 解析完整指令
    if buf_index > 0:
        cmd = bytes(rx_buffer[:buf_index]).decode('utf-8').strip()
        # 响应环境数据查询指令
        if cmd == "GET_ENV_DATA":
            resp = f"Env Data - Temp: {env_data_cache['temp']}C, Humidity: {env_data_cache['humi']}%\r\n"
            uart.write(resp)
        else:
            uart.write(f"Unknown command: {cmd}\r\n")
        # 重置缓冲区
        buf_index = 0

def collect_env_data() -> tuple[float, float]:
    """
​    模拟环境传感器(温湿度)数据采集函数(主任务核心业务)。

​    Args:

​    Notes:
​        - 温度模拟范围为18.0~35.0℃,湿度模拟范围为30.0~80.0%,保留2位小数。
​        - 采集数据后更新全局环境数据缓存,确保中断响应指令时获取最新数据。
​        - 返回值为元组格式,第一个元素为温度(℃),第二个为湿度(%)。
​        - 数据采集无硬件依赖,仅用于业务逻辑模拟。
​    """
​    ​global env_data_cache

    temp = round(random.uniform(18.0, 35.0), 2)
    humi = round(random.uniform(30.0, 80.0), 2)
    # 更新全局缓存
    env_data_cache["temp"] = temp
    env_data_cache["humi"] = humi
    return temp, humi

# ======================================== 初始化配置 ==========================================

# 上电延时3s
time.sleep(3)
# 打印调试消息
print("FreakStudio: Testing UART Recv IRQ_RX")

# 初始化状态LED(板载Pin25)- 指示数据采集状态
status_led = Pin(25, Pin.OUT)
status_led.off()

# 初始化UART0:TX=Pin0,RX=Pin1,波特率115200
uart0 = UART(0, baudrate=115200, tx=Pin(0), rx=Pin(1))
# 配置串口中断:接收空闲触发(IRQ_RXIDLE),软中断
uart0.irq(handler=uart_rxidle_handler, trigger=UART.IRQ_RXIDLE, hard=False)

# ========================================  主程序  ============================================

# 主循环:核心业务(环境数据采集)+ 等待中断响应
while True:
    # 真实业务任务1:采集环境数据
    current_temp, current_humi = collect_env_data()
    # 真实业务任务2:打印采集数据(调试)
    print(f"Collected - Temp: {current_temp}°C, Humidity: {current_humi}%")
    # LED闪烁指示采集动作
    status_led.on()
    time.sleep(0.1)
    status_led.off()
    # 模拟传感器采集间隔
    time.sleep(1)

在上面的代码中,我们进行如下的工作:

  1. 程序启动与初始化;
  2. 程序进入无限循环,以 1 秒为周期重复执行以下操作:
    1. 调用 collect_env_data 函数,模拟采集环境数据:生成 18.0~35.0℃ 的温度、30.0~80.0% 的湿度数据(均保留 2 位小数),并更新全局 env_data_cache 缓存;
    2. 打印当前采集的温湿度数据(调试用),控制状态 LED 点亮 0.1 秒后熄灭(直观指示数据采集动作);
    3. 延时 1 秒,模拟传感器的实际采集间隔,随后重复上述步骤。
  3. 当串口接收到完整指令(触发 “接收空闲” 中断)时,暂停主循环,执行中断处理逻辑:
    1. 读取串口所有未处理的字符,存入预分配的 128 字节接收缓冲区(限制索引防止越界);
    2. 将缓冲区数据解码为 UTF-8 字符串并去除首尾空白,解析指令内容;
    3. 指令匹配:若为 “GET_ENV_DATA”,则通过串口返回缓存中最新的温湿度数据;若为其他指令,返回 “Unknown command” 提示;
    4. 重置缓冲区索引,中断处理完成,程序回到主循环继续执行数据采集任务。

下面是整体的流程图:

打开 XCOM 软件,选择 CH340K 芯片对应 COM 口:

串口通信参数和程序中一致:

其余发送和接收参数默认即可:

点击打开串口:

烧录程序,打开终端,显示如下:

可以看到,核心板上的 LED 灯定时闪烁,同时串口指令响应功能也符合预期(发送 GET_ENV_DATA 能返回对应温湿度、发送 OTHER_COMMAND 会提示未知指令),整体功能运行正常。

3.发送空闲中断(IRQ_TXIDLE):确认长数据发送完成

串口发送空闲中断(IRQ_TXIDLE)是串口通信中针对数据发送环节的核心中断机制,具体指当串口发送端(TX)将发送缓冲区中所有待发送字节全部发送完毕,发送缓冲区为空且 TX 引脚进入无数据输出的空闲状态时触发该中断;其核心价值在于精准确认 “一段长数据 / 一帧完整数据的发送流程彻底完成”,而非仅触发发送动作,尤其适配需要发送大尺寸数据(如批量传感器日志、长文本响应、二进制数据流)的场景,能有效避免因未确认发送完成就修改缓冲区、关闭串口或启动下一次发送导致的数据丢失 / 错乱问题。

它与接收空闲中断(IRQ_RXIDLE)形成对应:

  • IRQ_RXIDLE 聚焦 “完整数据帧接收完成”;
  • IRQ_TXIDLE 聚焦 “完整数据帧发送完成”。

在下面代码中,我们利用 IRQ_TXIDLE 中断标记长日志发送接近完成,结合批量缓存机制实现模拟传感器日志的可靠串口发送。

完整代码如下:

# Python env   : MicroPython v1.27.0
# -*- coding: utf-8 -*-        
# @Time    : 2025/12/28 上午11:12   
# @Author  : 李清水            
# @File    : main.py       
# @Description : 串口发送空闲中断(IRQ_TXIDLE)- 长日志传输状态提示

# ======================================== 导入相关模块 =========================================

# 导入MicroPython标准库模块
from machine import Pin, UART
# 导入时间模块
import time
# 用于模拟传感器数据
import random

# ======================================== 全局变量 ============================================

# 传感器日志缓存(积累6条后发送,确保长度>5触发TXIDLE)
sensor_logs = []
# 日志编号
log_seq = 1
# 发送状态:IDLE(空闲)/SENDING(发送中)
tx_status = "IDLE"
# TXIDLE中断触发标志
txidle_flag = False

# ======================================== 功能函数 ============================================

def uart_txidle_handler(uart: UART) -> None:
    """
​    RP2串口TXIDLE中断处理函数(软中断),仅标记中断触发状态避免硬件抢占。

​    Args:
​        uart (UART): 触发中断的UART实例(machine.UART类型)。

​    Notes:
​        - RP2的TXIDLE中断在剩余5个字符待发送时触发,非发送完成状态。
​        - 仅在发送中(SENDING)状态下标记,避免空触发干扰状态机。
​        - 不执行UART写入操作,防止硬件抢占导致数据错乱。
​    """
​    ​global txidle_flag
    # 仅在发送中标记
    if tx_status == "SENDING":
        txidle_flag = True

def generate_sensor_log() -> str:
    """
​    生成固定格式的模拟传感器日志,适配RP2 UART缓冲区特性。

​    Args:
​        None

​    Notes:
​        - 固定字段格式确保单条日志字符数一致,避免长度波动导致数据截断。
​        - 日志编号4位补零,防止编号循环过快导致视觉混淆。
​        - 编号超过9999时重置,避免数值溢出影响日志可读性。

​    Return:
​        (str): 拟传感器日志文件,格式固定。
​    """
​    ​global log_seq

    temp = round(random.uniform(20.0, 40.0), 2)
    pressure = round(random.uniform(950.0, 1050.0), 2)
    # 强制固定格式,确保每条日志长度一致(避免截断)
    log = f"LOG_{log_seq:02d} | Time: {int(time.time())}s | Temp: {temp:5.2f} degC | Pressure: {pressure:6.2f} hPa"
    log_seq += 1
    return log

def send_log_batch(uart: UART) -> None:
    """
​    RP2串口3条日志批量发送函数,适配硬件FIFO缓冲区容量(防截断)。

​    Args:
​        uart (UART): 用于发送日志的UART实例(machine.UART类型)。

​    Notes:
​        - 批量阈值设为3条,总字符数适配RP2 UART FIFO缓冲区,避免数据积压。
​        - 使用uart.flush()阻塞等待,替代不可靠的uart.txdone()确保发送完成。
​        - 仅在IDLE状态下执行发送,避免多线程式状态竞态导致数据错乱。
​        - 一次性写入整批日志,保证硬件层面的原子发送操作。

​    Return:
​        None
​    """
​    ​global sensor_logs, tx_status, txidle_flag

    if len(sensor_logs) >= 3 and tx_status == "IDLE":
        tx_status = "SENDING"

        # 重置中断标志
        txidle_flag = False

        # 拼接整批日志(原子块,一次性发送)
        long_log = "\r\n".join(sensor_logs) + "\r\n===== LOG BATCH END =====\r\n"
        print(f"Sending {len(sensor_logs)} logs...")

        # 一次性写入整批日志(硬件原子操作)
        uart.write(long_log)

        # 阻塞直到整批日志发送完成
        while uart.flush():
            time.sleep_ms(5)

        # 发送完成后,终端输出TXIDLE提示
        if txidle_flag:
            print("[STATUS] Transmission nearly complete (TXIDLE triggered)")
            txidle_flag = False

        # 清空缓存+恢复状态
        sensor_logs.clear()
        tx_status = "IDLE"

# ======================================== 初始化配置 ==========================================

# 上电延时3s
time.sleep(3)
# 打印调试消息
print("FreakStudio: Testing UART Transmit IRQ_TXIDLE")

# 初始化发送状态LED(板载Pin25)
tx_led = Pin(25, Pin.OUT)
tx_led.off()

# 初始化UART0:TX=Pin0,RX=Pin1,波特率115200
uart0 = UART(0, baudrate=115200, tx=Pin(0), rx=Pin(1))
# 配置串口中断:发送空闲触发(IRQ_TXIDLE),软中断
uart0.irq(handler=uart_txidle_handler, trigger=UART.IRQ_TXIDLE, hard=False)

# ========================================  主程序  ============================================

while True:
    # 仅空闲时生成日志(避免缓冲区过载)
    if tx_status == "IDLE":
        log = generate_sensor_log()
        sensor_logs.append(log)
        print(f"Generated: {log}")

    # 批量发送(阻塞直到完成)
    send_log_batch(uart0)

    # 更新LED和状态打印
    tx_led.value(1 if tx_status != "IDLE" else 0)
    print(f"Transmit Status: {tx_status}")

    # 固定间隔(降低循环频率,减少冲突)
    time.sleep(2)

在上面的代码中,我们进行了如下工作:

  1. 程序启动与初始化;
  2. 程序进入无限循环,以 2 秒为周期重复执行以下操作:
    1. 日志生成:仅当发送状态(tx_status)为 IDLE(空闲)时,调用 generate_sensor_log 生成固定格式的模拟温压传感器日志(含日志编号、时间戳、温度、气压),将日志添加到缓存列表 sensor_logs 并打印生成信息;
    2. 批量发送:调用 send_log_batch 函数,若缓存日志 ≥3 条且处于 IDLE 状态,执行批量发送逻辑:
      1. 切换 tx_status 为 SENDING,重置 TXIDLE 中断标志 txidle_flag
      2. 拼接缓存中的 3 条日志为整段长文本(带 “LOG BATCH END” 结束标识),通过 UART0 一次性写入发送(保证硬件原子操作,避免数据截断);
      3. 调用 uart.flush() 阻塞等待,直到所有数据发送完成(期间每 5ms 检测一次,替代不可靠的 uart.txdone());
      4. 若 TXIDLE 中断触发(txidle_flag 为 True),打印 “发送接近完成” 提示并重置标志;
      5. 清空日志缓存,将 tx_status 恢复为 IDLE;
    3. 状态指示:根据 tx_status 控制 LED(SENDING 点亮、IDLE 熄灭),打印当前发送状态;
    4. 延时 2 秒:降低循环频率,减少发送冲突,适配硬件缓冲区特性。
  3. TXIDLE 中断响应:当 UART0 发送缓冲区剩余 5 个字符待发送(RP2 硬件特性)触发 IRQ_TXIDLE 中断时,执行 uart_txidle_handler 函数:仅在 tx_status 为 SENDING 时,将 txidle_flag 标记为 True(仅标记状态,不执行写入操作,避免硬件抢占导致数据错乱),供主循环判定发送接近完成的状态。

完整流程图如下:

打开 XCOM 软件,选择 CH340K 芯片对应 COM 口:

串口通信参数和程序中一致:

其余发送和接收参数默认即可:

点击打开串口:

烧录程序,打开终端,显示如下:

我们可以看到,程序运行效果完全符合预期:

  1. 主循环周期性生成固定格式的模拟传感器日志(左侧终端持续输出 “Generated” 对应的日志信息),当日志缓存累积到 3 条时自动进入发送状态(终端显示 “Sending 3 logs”);
  2. 发送过程中触发 IRQ_TXIDLE 中断,终端同步打印 “Transmission nearly complete” 的回调提示( “串口发送中断触发回调函数”),同时右侧串口助手完整接收到带 “LOG BATCH END” 标识的批量日志;
  3. 发送完成后状态恢复为 IDLE。

日志生成 - 缓存 - 发送流程循环有序,既验证了 TXIDLE 中断对长数据发送状态的标记功能,也保障了批量日志的可靠串口传输。

在 RP2 平台使用 MicroPythonUART 发送空闲中断(IRQ_TXIDLE)时,核心注意事项是需严格控制单次批量发送的总字符数,使其适配 RP2 UART 硬件发送缓冲区(FIFO)的有限容量:

  • 若总字符数远超该容量,数据会积压在软件缓冲区,而 RP2 的 TXIDLE 中断仅在剩余 5 个字符待发送时触发,若搭配不可靠的 uart.txdone()(仅判断单字符发送寄存器为空,不检测 FIFO)易误判发送完成,提前修改发送状态、清空缓存,干扰硬件最后的发送流程导致数据截断;
  • 因此需将单次发送字符量控制在 FIFO 安全承载区间,优先使用 ​uart.flush()(阻塞至所有数据含 FIFO 内的完全发送)替代uart.txdone(),同时关闭中断的hard=True改用软中断 ,避免硬件中断抢占加剧数据错乱,确保中断触发与发送状态管理的同步性。

posted @ 2026-08-31 16:52  FreakStudio  阅读(5)  评论(0)    收藏  举报