跳转至

21 ioqueue io序列操作

作者:孟伟 | 最后修改:2026-04-14

一、ioqueue 概述

ioqueue 是 LuatOS 的高精度硬件定时器队列控制核心库,主要用于实现精确的时序控制和信号捕获。该核心库适用于需要高精度时序控制的场景,如单总线通信、精确脉冲生成等;可以通过硬件定时器精确控制 GPIO 的操作时序。

1.1 ioqueue 有什么用?

ioqueue 核心库为物联网设备提供硬件级的高精度时序控制能力,主要功能包括:

  • 高精度时序控制:实现 20us 级别的精确延时和时序控制
  • 硬件级信号捕获:精确捕获外部信号的边沿变化和时间戳
  • 复杂波形生成:生成各种复杂的脉冲波形和通信时序
  • 单总线协议支持:完美支持 DHT11、DS18B20 等单总线设备
  • 非阻塞操作:基于硬件定时器,不占用主线程资源

二、演示功能概述

本教程将使用支持 ioqueue 的开发板演示该核心库的核心功能,主要包括:

(1)DHT11 温湿度传感器数据读取

(2)高精度固定间隔脉冲输出

(3)高精度可变间隔脉冲输出

三、准备硬件环境

1、Air780EHV 核心板一块 + DHT11 传感器一个:

2、TYPE-C USB 数据线一根 ,Air780EHV 核心板和数据线的硬件接线方式为:

  • Air780EHV 核心板通过 TYPE-C USB 口供电;(外部供电/USB 供电 拨动开关 拨到 USB 供电一端)
  • TYPE-C USB 数据线直接插到核心板的 TYPE-C USB 座子,另外一端连接电脑 USB 口;
  • DHT11 接线如下:
DHT11
Air780EHV
VCC
VDD_EXT
DATA
GPIO25
GND
GND

四、软件环境

在开始实践本示例之前,先筹备一下软件环境:

1.烧录工具: Luatools 工具

2.内核固件:本demo开发测试时使用的固件为Air780EHV V2016 版本固件,本demo对固件版本没有什么特殊要求,所以你如果要测试本demo时,可以直接使用最新版本的内核固件;如果发现最新版本的内核固件测试有问题,可以使用我们开发本demo时使用的内核固件版本来对比测试;

3.LuatOS 需要的脚本和资源文件

脚本和资源文件:https://gitee.com/openLuat/LuatOS/tree/master/module/Air780EHM_Air780EHV_Air780EGH/demo/ioqueue

准备好软件环境之后,接下来查看如何烧录项目文件到 Air780EHV 开发板,将本篇文章中演示使用的项目文件烧录到 Air780EHV 开发板中。

五、API 接口说明

详细 API 文档请参考:https://docs.openluat.com/osapi/core/ioqueue/

六、代码示例介绍

下面将演示使用 ioqueue 的几种典型应用场景。

6.1 DHT11 温湿度传感器读取

单总线协议要求严格的时序控制、主机需要先发送特定启动信号、传感器响应时间精确到微秒级、数据通过脉冲宽度编码

使用流程:

dht11 通讯协议介绍:

数据分为小数部分和整数部分,一次完整的数据传输为 40bit,高位先出

数据格式:8bit 湿度整数数据 +8bit 湿度小数数据 +8bit 温度整数数据 +8bit 温度小数数据 +8bit 校验

8bit 校验 = 8bit 湿度整数数据 +8bit 湿度小数数据 +8bit 温度整数数据 +8bit 温度小数数据

校验可以判断数据是否正确发送

DHT11 工作时序:

  • 主机发送起始信号以后,DHT11 发送响应信号,然后发送 40bit 的数据,高位在前

总时序图如下:

  • 起始信号:总线空闲状态由 DHT11 内置上拉电阻拉高,主机拉低总线至少 18ms释放总线 20-40us
  • DHT11 响应:存在的 DHT11 会及时响应主机,同时拉低总线 80us 后,释放总线 80us,然后拉低总线,表示开始传送数据

  • 发送数据:当总线是低电平时表示开始发送数据,同时存在 50us 低电平时隙,之后拉高总线,高电平的持续时间表示发送 0 或者 1,当高电平持续时间为 26us-28us 表示发送 0,高电平持续时间为 70us 时,表示发送 1
  • 数据发送完毕,由上拉电阻拉高,置回空闲高电平状态

-- 测试单总线DHT11
-- 第一步:确保硬件定时器空闲
ioqueue.stop(hw_timer_id)

-- 第二步:初始化io队列
ioqueue.init(hw_timer_id, 100, 1)


-- 第三步:初始状态设置
ioqueue.setgpio(hw_timer_id, capture_pin, true, gpio.PULLUP)
-- 参数详解:
-- 10000: 延时10ms(10000微秒)
-- 0: 时间微调值
-- false: 单次延时(非连续模式)
-- 作用:初始空闲状态时长为10ms,给传感器足够的准备时间
ioqueue.setdelay(hw_timer_id, 10000, 0, false)


-- 第四步:配置DHT11传感器的启动信号,主机主动拉低总线开始通信
ioqueue.setgpio(hw_timer_id, capture_pin, false, 0, 0)

-- 18000: 延时18ms,这是DHT11协议要求的启动信号最小时间
ioqueue.setdelay(hw_timer_id, 18000, 0, false)

-- 第五步:配置捕获参数,为接收传感器数据做准备,此命令仅配置参数,实际捕获需配合后续的capture()命令执行
-- 参数详解:
-- gpio.PULLUP: 设置引脚为上拉输入模式(释放总线控制权)
-- gpio.FALLING: 只捕获下降沿,捕获到后会记录io编号,电平高低,以及时间
-- 100000 * tick_us: 单个capture()命令的最大等待时间100ms,超时后继续执行后续命令,每个capture()都是独立的100ms等待窗口
ioqueue.set_cap(hw_timer_id, capture_pin, gpio.PULLUP, gpio.FALLING, 100000 * tick_us)
--[[关于一个捕获周期含义:
set_cap不等于开始捕获,只是配置参数捕获参数
其中set_cap配置的最大等待时间是防止因传感器故障导致程序永久卡住
开始:当执行 ioqueue.capture() 命令时开始一个捕获周期
结束:满足以下任一条件时结束:
    检测到下降沿 → 立即记录时间戳并结束本次捕获周期,然后执行下一条命令
    达到100ms超时 → 直接结束,不记录数据,然后执行下一条命令
    调用cap_done() → 强制结束命令队列
以上面代码为例:
在100ms内,一次下降沿也没有检测到,则直接结束,执行队列中的下一条命令
在100ms内,检测到一次下降沿,立即记录时间戳并结束,然后执行队列中的下一条命令;
]]

-- 第六步:预分配捕获缓冲区 ,对io操作队列增加42次捕获IO状态命令
for i = 1, 42, 1 do
   ioqueue.capture(hw_timer_id)
end

-- 停止捕获,不再监听该引脚的边沿变化
ioqueue.cap_done(hw_timer_id, capture_pin)

-- 恢复数据线为上拉输入状态,释放总线
ioqueue.setgpio(hw_timer_id, capture_pin, true, gpio.PULLUP)


-- 第七步:执行整个命令序列
-- 开始按顺序执行前面设置的所有命令
ioqueue.start(hw_timer_id)

-- 等待执行完成,系统会在完成时发布这个事件
-- 这是异步操作,不会阻塞其他任务
sys.waitUntil("IO_QUEUE_DONE_"..hw_timer_id)

-- 停止硬件定时器
ioqueue.stop(hw_timer_id)


-- 第八步:读取捕获的数据
cnt1, cnt2 = ioqueue.get(hw_timer_id, buff1, buff2)
-- 参数详解:
-- buff1: 存储输入数据的缓冲区
-- buff2: 存储捕获数据的缓冲区
-- cnt1: 读取io数据的数量,此返回值对应ioqueue.input()接口所配置的对读取gpio命令数量,此代码中是nil
-- cnt2: 捕获数据的数量(应该是42)

if cnt2 ~= 42 then
   log.info('test fail')
   goto TEST_OUT
end
-- 如果捕获数据不是42个,说明通信失败

-- 第九步:解析数据
-- 从捕获缓冲区读取第二个下降沿的时间戳
-- 数据结构:每个捕获点占6字节
-- 字节0: GPIO ID编号
-- 字节1: 电平状态(0=下降沿,1=上升沿)
-- 字节2-5: 32位时间戳(4字节)
-- 所以第二个捕获点在偏移量6处,时间戳在6+2处
lastTick = buff2:query(6 + 2, 4, false)


j = 0
bit = 8
buff1[0] = 0

for i = 2, 41, 1 do  -- 遍历40个数据位(跳过第1个下降沿的DHT11响应信号)
    -- 验证数据完整性
    if buff2[i * 6 + 0] ~= capture_pin or buff2[i * 6 + 1] ~= 0 then
         log.error("capture", i, buff2[i * 6 + 0], buff2[i * 6 + 1])
    end

    -- 计算时间间隔
    -- 当前位的时间戳
    nowTick = buff2:query(i * 6 + 2, 4, false)
    -- 左移1位,为新的数据位腾出空间
    buff1[j] = buff1[j] << 1

    -- DHT11数据编码原理:
    -- 每个数据位都以50us低电平开始
    -- 然后高电平持续时间不同:
    --   26-28us → 数据0
    --   70us    → 数据1
    -- bit1Tick 是100us阈值,总时间 > 100us → 判断为位1,≤ 100us → 判断为位0
    if (nowTick - lastTick) > bit1Tick then
         buff1[j] = buff1[j] + 1  -- 设置最低位为1
     end

     bit = bit - 1
     if bit == 0 then  -- 完成1字节(8位)
         j = j + 1     -- 移动到下一字节
         bit = 8       -- 重置位计数器
     end
     lastTick = nowTick  -- 更新参考时间戳
end

-- 第十步:数据校验
buff1[5] = buff1[0] + buff1[1] + buff1[2] + buff1[3]
-- DHT11协议:第5字节是前4字节的校验和
if buff1[4] ~= buff1[5] then
      log.info('check fail', buff1[4], buff1[5])
else
       log.info("湿度", buff1[0] .. '.' .. buff1[1], "温度", buff1[2] .. '.' ..  buff1[3])
       -- buff1[0]: 湿度整数部分
       -- buff1[1]: 湿度小数部分
       -- buff1[2]: 温度整数部分
       -- buff1[3]: 温度小数部分
       -- buff1[4]: 校验和
end

::TEST_OUT::
-- 释放硬件定时器资源,可以重新分配使用
ioqueue.release(hw_timer_id)

6.2 输出精确脉冲场景

需要不同的脉冲宽度、软件循环无法满足精度要求、需要连续输出大量脉冲

使用流程:

下面演示利用硬件定时器生成固定频率或可变宽度的精确脉冲序列,支持连续和单次延时模式,实现微秒级脉冲控制。

6.2.1 高精度固定间隔脉冲输出

输出脉冲信息:

输出固定间隔对称方波

  • 低电平持续时间:20 微秒(固定)
  • 高电平持续时间:20 微秒(固定)
  • 脉冲周期:40 微秒(完整周期)
  • 占空比 50%
  • 脉冲数量:41 个完整周期(通过循环 40 次生成)
  • 使用 ioqueue.setdelay 的连续模式,所有延时间隔自动保持 20us
-- 第一步:初始化
-- 100个命令,循环1次(生成100个脉冲)
ioqueue.init(hw_timer_id, 100, 1)

-- 设置GPIO为输出模式,初始输出高电平
ioqueue.setgpio(hw_timer_id, out_pin, false, 0, 1)


-- 第二步:配置连续延时模式
ioqueue.setdelay(hw_timer_id, 20, tick_us - 3, true)


-- 第三步:生成脉冲序列
-- 每个循环生成1个完整周期:低电平20us + 高电平20us = 40us周期
for i = 0, 40, 1 do
    ioqueue.output(hw_timer_id, out_pin, 0)  -- 输出低电平
    ioqueue.delay(hw_timer_id)               -- 延时20us
    ioqueue.output(hw_timer_id, out_pin, 1)  -- 输出高电平
    ioqueue.delay(hw_timer_id)               -- 延时20us
end



-- 第四步:执行
ioqueue.start(hw_timer_id)
-- 等待执行完成,系统会在完成时发布这个事件
sys.waitUntil("IO_QUEUE_DONE_"..hw_timer_id)
-- 停止硬件定时器
ioqueue.stop(hw_timer_id)
-- 释放硬件定时器资源,可以重新分配使用
ioqueue.release(hw_timer_id)

6.2.2 高精度可变间隔脉冲输出

输出脉冲信息:

输出可变间隔非对称脉冲

  • 10 次完整序列
  • 输出波形:低电平 20us → 高电平 30us → 低电平 40us → 高电平 50us→ 低电平 60us → 高电平 70us
  • 使用 ioqueue.setdelay 单次模式,每个延时独立配置
log.info('output 2 start')
--测试高精度可变间隔定时输出
ioqueue.init(hw_timer_id, 100, 10)
--设置成输出口,电平1
ioqueue.setgpio(hw_timer_id, out_pin, false, 0, 1)
--单次延迟20us,如果不准,对time_tick微调
ioqueue.setdelay(hw_timer_id, 20, tick_us - 3)
--低电平
ioqueue.output(hw_timer_id, out_pin, 0)
--单次延迟30us
ioqueue.setdelay(hw_timer_id, 30, tick_us - 3)
--高电平
ioqueue.output(hw_timer_id, out_pin, 1)
--单次延迟40us
ioqueue.setdelay(hw_timer_id, 40, tick_us - 3)
--低电平
ioqueue.output(hw_timer_id, out_pin, 0)
--单次延迟50us
ioqueue.setdelay(hw_timer_id, 50, tick_us - 3)
--高电平
ioqueue.output(hw_timer_id, out_pin, 1)
--单次延迟60us
ioqueue.setdelay(hw_timer_id, 60, tick_us - 3)
--低电平
ioqueue.output(hw_timer_id, out_pin, 0)
--单次延迟70us
ioqueue.setdelay(hw_timer_id, 70, tick_us - 3)
--高电平
ioqueue.output(hw_timer_id, out_pin, 1)
ioqueue.start(hw_timer_id)
sys.waitUntil("IO_QUEUE_DONE_" .. hw_timer_id)
log.info('output 2 done')
ioqueue.stop(hw_timer_id)
ioqueue.release(hw_timer_id)
sys.wait(500)

6.3 功能验证

6.3.1 Luatools 日志输出

ioqueue 操作执行状态日志

6.3.2 DHT11 数据读取验证

DHT11 温湿度数据读取波形

6.3.3 脉冲输出波形验证

使用逻辑分析仪观察生成的精确脉冲波形:

固定间隔脉冲输出:

可变间隔脉冲输出:

七、总结

至此,本示例详细介绍了如何使用 ioqueue 核心库实现高精度时序控制和信号捕获。ioqueue 提供了硬件级的高精度控制能力,特别适合需要精确时序的应用场景。