跳转至

usbapp USB HID / U 盘应用封装

一、概述

usbapp 是 LuatOS 里较早的一层 USB 应用接口封装,只做两件事:把模组枚举成 USB HID 从机(标准键盘,或自定义 HID 报文),以及把模组的 TF 卡以 U 盘形式暴露给主机。它不负责主机模式,也不做 CDC/网卡/音频。

要点:

1、模块名为 usbapp,由固件编译开关 LUAT_USE_USB 控制。和它同宏控制的还有通用 USB 栈 usb(见 usb 核心库)。两者的关系是:usb 是后出的通用实现,能用 usb.add_class 自由组合 CDC-ACM / MSC / HID / ECM / RNDIS / UAC / UVC;usbapp 是更早的固定封装,接口只有 HID 与 U 盘两类,参数也更少;

2、usbapp 里所有函数的第一个参数 id 目前都是占位:源码内部固定使用设备 0(USB_ID0),传 1、2、3 不会报错,也不会切到第二个设备。多设备场景请改用 usb 库的 usb.mode(id, ...) 系列接口;

3、HID 的状态变化通过系统消息 USB_HID_INC 上报,事件值与本文常量一一对应(见 系统消息)。键盘模式上传走 vhid_upload,自定义模式上传走 hid_tx,两者不能混用;

4、usbapp.hid_rx 在源码注释里明确写着「目前只有 air105 支持这个操作」,其他平台调用会返回 0,接收侧请改走 usb.rx 或 USB_HID_INC 事件;

⚠️ 用之前先自检:usbapp 是早期接口,公开的 Air160x 系列固件跑的是 usb 核心库(HID 拆成 usb.HID_CM 与 usb.HID_KB 两个类),并不包含 usbapp。脚本里先判断一下,避免直接报 nil:

if usbapp then
    log.info("usbapp", "当前固件包含 usbapp")
elseif usb then
    log.info("usbapp", "请改用 usb 核心库实现 HID")
else
    log.info("usbapp", "当前固件既没有 usbapp 也没有 usb")
end

二、核心示例

2.1 标准键盘模式:模拟敲键

-- 键盘模式,报文长度固定 8 字节(不足会自动按 8 处理)
usbapp.hid_mode(0, 0)
usbapp.start(0)

local hid_ready = false
sys.subscribe("USB_HID_INC", function(event)
    if event == usbapp.READY then
        hid_ready = true
        log.info("usbapp", "主机已就绪,可以发送")
    elseif event == usbapp.SEND_OK then
        log.info("usbapp", "上一包发送完成")
    end
end)

-- 等主机枚举完成再发,避免第一包丢掉
local timeout = 50
while not hid_ready and timeout > 0 do
    sys.wait(100)
    timeout = timeout - 1
end
if hid_ready then
    usbapp.vhid_upload(0, "1234")   -- 主机侧会看到依次敲出 1 2 3 4
else
    log.warn("usbapp", "5 秒内没等到 READY,检查线材与主机")
end

2.2 自定义 HID 模式:和上位机收发原始报文

-- mode = 1 是自定义 HID;buff_size 只能是 8/16/32/64
usbapp.hid_mode(0, 1, 64)
usbapp.start(0)

-- 发一包 64 字节,数据不足由底层补 0
usbapp.hid_tx(0, "PING")

-- 收:只有 air105 支持 buff 形式读取
local buff = zbuff.create(128)
local n = usbapp.hid_rx(0, buff)
if n and n > 0 then
    log.info("usbapp", "收到", n, buff:query())
end

2.3 U 盘模式:把 TF 卡交给主机

-- 先把 TF 卡挂到 fatfs,再挂给 USB 主机
fatfs.mount(fatfs.SPI, "/sd")
usbapp.start(0)
usbapp.udisk_attach_sdhc(0)

-- 拔盘前先 detach,再 umount,避免主机写坏文件系统
usbapp.udisk_detach_sdhc(0)
fatfs.umount("/sd")

三、常量详解

usbapp 的四个常量就是 USB_HID_INC 系统消息的事件值,用来判断 HID 链路状态:

常量 值 含义 典型处理
usbapp.NO_READY 0 HID 未就绪(主机没枚举、或线没插好) 等待,不要连续发数据
usbapp.READY 1 HID 已就绪,可以发送 置标志位,开始发数据
usbapp.SEND_OK 2 上一包发送完成 可以发下一包(避免连发丢包)
usbapp.NEW_DATA 3 主机发来了新数据 去 hid_rx 或 usb.rx 取数据
sys.subscribe("USB_HID_INC", function(event)
    if event == usbapp.NEW_DATA then
        log.info("usbapp", "主机有数据下发")
    end
end)

注意:这几个常量只是事件编号,不会因为 hid_mode 不同而变化;HID 是否真的建立了链路,仍要看 READY 有没有来。

四、函数详解

4.1 usbapp.set_id(id, vid, pid)

功能

设置 USB 设备描述符里的 VID / PID。VID、PID 默认是 0x1234 / 0x5678,量产前要改成自己的值,否则同一台电脑上装多个设备会因 VID/PID 相同而出现驱动冲突。

参数

id

参数含义:设备 id
数据类型:number
取值范围:任意整数(当前实现恒用 0)
是否必选:否,默认 0
注意事项:传其他值不会报错,但也不会切设备。

vid

参数含义:厂商 ID
数据类型:number
取值范围:0 ~ 0xFFFF
是否必选:否,默认 0x1234
注意事项:按 16 位数值传,例如 0x1234;底层按 USB 描述符的小端格式写入。

pid

参数含义:产品 ID
数据类型:number
取值范围:0 ~ 0xFFFF
是否必选:否,默认 0x5678
注意事项:同一 VID 下 PID 不能和已有设备重复。

返回值

无。

示例

usbapp.set_id(0, 0x33AA, 0x0001)
usbapp.start(0)

提示:VID 需要向 USB-IF 申请,个人项目常用厂商提供的测试 VID;正式产品不要随意占用别人的 VID。

4.2 usbapp.hid_mode(id, mode, buff_size)

功能

选择 HID 工作模式并指定报文长度。必须在 usbapp.start 之前调用,启动后再改不会生效。

参数

id

参数含义:设备 id
数据类型:number
取值范围:任意整数(当前实现恒用 0)
是否必选:否,默认 0

mode

参数含义:HID 模式
数据类型:number
取值范围:0 = 标准键盘,1 = 自定义 HID
是否必选:否,默认 0
注意事项:0 走键盘描述符,只能用 vhid_upload 发;1 走自定义描述符,用 hid_tx 发原始字节。

buff_size

参数含义:每包报文长度
数据类型:number
取值范围:只能是 8、16、32、64
是否必选:否,默认 8
注意事项:键盘模式或填了非法值时,会自动按 8 处理;上位机要按同样的长度分包解析。

返回值

无。

示例

usbapp.hid_mode(0, 1, 64)   -- 自定义 HID,64 字节一包
usbapp.start(0)

4.3 usbapp.start(id)

功能

启动 USB 从机,让主机开始枚举。

参数

id

参数含义:设备 id
数据类型:number
取值范围:任意整数(当前实现恒用 0)
是否必选:否,默认 0
注意事项:函数不检查 id,也不会因为 id 非法而失败。

返回值

含义说明:启动结果
数据类型:boolean
取值范围:true
返回示例:true

⚠️ 这个返回值是固定的 true,不代表真的枚举成功。判断链路是否可用,要看有没有收到 USB_HID_INC 的 READY 事件。

示例

usbapp.start(0)

4.4 usbapp.stop(id)

功能

关闭 USB 从机,主机侧会看到设备掉线。

参数

id

参数含义:设备 id
数据类型:number
取值范围:任意整数(当前实现恒用 0)
是否必选:否,默认 0

返回值

含义说明:关闭结果
数据类型:boolean
取值范围:true
返回示例:true

同样恒为 true,不能当「已成功关闭」的依据。重新枚举要 stop 后再 start。

示例

usbapp.stop(0)
usbapp.start(0)   -- 重新枚举

4.5 usbapp.vhid_upload(id, data)

功能

键盘模式下把字符串「敲」到主机上。底层把字符逐个映射成 HID 键盘报文,所以它模拟的是按键,不是文本注入。

参数

id

参数含义:设备 id
数据类型:number
取值范围:任意整数(当前实现恒用 0)
是否必选:否,默认 0

data

参数含义:要敲出的内容
数据类型:string
取值范围:键盘 HID 字符集内的可见字符
是否必选:是
注意事项:HID 键盘可用字符有限,不支持的字符会被替换成空格;中文、emoji 都不在支持范围。长度大于 0 才会真正发送。

返回值

含义说明:是否已提交发送
数据类型:boolean
取值范围:true / false
返回示例:true

示例

usbapp.hid_mode(0, 0)
usbapp.start(0)
usbapp.vhid_upload(0, "hello\r")   -- 回车用 \r 表示

4.6 usbapp.vhid_cancel_upload(id)

功能

取消上一次尚未发完的键盘上传。正常流程里用不到,只有在上传过程中要紧急停止(例如误输入、要立刻改发别的按键)时才需要。

参数

id

参数含义:设备 id
数据类型:number
取值范围:任意整数(当前实现恒用 0)
是否必选:否,默认 0

返回值

含义说明:无返回值
数据类型:nil
返回示例:nil

示例

usbapp.vhid_upload(0, "1234567890")
usbapp.vhid_cancel_upload(0)   -- 发到一半叫停

4.7 usbapp.hid_tx(id, data, start, len)

功能

自定义 HID 模式下发送原始报文。支持直接传字符串,也支持从 zbuff 里取一段发送。

参数

id

参数含义:设备 id
数据类型:number
取值范围:任意整数(当前实现恒用 0)
是否必选:否,默认 0

data

参数含义:要发送的数据
数据类型:string 或 zbuff
取值范围:长度不超过端点包长的数据会被底层补 0 到包长
是否必选:是
注意事项:传 zbuff 时才能用后两个参数指定起止位置。

start

参数含义:从 zbuff 的第几个字节开始
数据类型:number
取值范围:0 ~ zbuff 长度
是否必选:否,默认 0
注意事项:仅当 data 是 zbuff 时有效;start 超出 zbuff 长度直接返回 false。

len

参数含义:要发送的字节数
数据类型:number
取值范围:1 ~ zbuff 内有效长度
是否必选:否,默认取 zbuff 的有效数据长度
注意事项:仅当 data 是 zbuff 时有效;start + len 越界时自动截断到缓冲区末尾。

返回值

含义说明:是否已提交发送
数据类型:boolean
取值范围:true / false
返回示例:true

示例

usbapp.hid_mode(0, 1, 64)
usbapp.start(0)

usbapp.hid_tx(0, "PING")          -- 字符串,底层补 0 到 64 字节

local buff = zbuff.create(32)
buff:write("0123456789")
usbapp.hid_tx(0, buff, 2, 5)      -- 只发 "23456"

4.8 usbapp.hid_rx(id, buff)

功能

把收到的 HID 数据一次性读进 zbuff。只有 air105 支持这个操作,其他平台读到的长度恒为 0。

参数

id

参数含义:设备 id
数据类型:number
取值范围:任意整数(当前实现恒用 0)
是否必选:是

buff

参数含义:接收缓冲区
数据类型:zbuff
取值范围:任意 zbuff;空间不够时会自动扩容
是否必选:是
注意事项:必须传 zbuff,传字符串或其他类型直接返回 0;读完 zbuff 的 used 会往后移动,可以直接 query 出本次数据。

返回值

含义说明:本次读到的字节数
数据类型:number
取值范围:0 表示没有数据或平台不支持
返回示例:16

示例

local buff = zbuff.create(64)
local n = usbapp.hid_rx(0, buff)
if n > 0 then
    log.info("usbapp", "rx", buff:query())
end

4.9 usbapp.udisk_attach_sdhc(id)

功能

把 TF 卡(SDHC)以 U 盘形式挂给 USB 主机。挂之前要先在脚本里把卡挂到 fatfs,否则主机看到的是一个不能用的空盘。

参数

id

参数含义:设备 id
数据类型:number
取值范围:任意整数(当前实现恒用 0)
是否必选:否,默认 0

返回值

含义说明:无返回值
数据类型:nil
返回示例:nil

示例

fatfs.mount(fatfs.SPI, "/sd")
usbapp.start(0)
usbapp.udisk_attach_sdhc(0)

⚠️ 挂成 U 盘期间,脚本和主机不要同时写这张卡,两边缓存互不知道,必然写坏。主机侧要安全弹出后再 detach。

4.10 usbapp.udisk_detach_sdhc(id)

功能

取消 U 盘挂载,把卡交回脚本侧。

参数

id

参数含义:设备 id
数据类型:number
取值范围:任意整数(当前实现恒用 0)
是否必选:否,默认 0

返回值

含义说明:无返回值
数据类型:nil
返回示例:nil

示例

usbapp.udisk_detach_sdhc(0)
fatfs.umount("/sd")

五、产品支持与固件差异

usbapp 由 LUAT_USE_USB 宏控制,属于早期 USB 封装:

  • 当前公开的 Air160x 系列(Air1601/Air1602/Air1603,以及共用 160X 固件的 Air8601/Air8602/Air8605/Air8607)走的是 usb 核心库,HID 用 usb.HID_CM / usb.HID_KB 加 usb.tx / usb.rx 实现,并不包含 usbapp;判定依据见 usb 核心库 的产品支持说明与对应固件版本页;
  • EC7xx 系列(Air780E 系、Air780EPM/EGP、Air700E 系、Air8000 系、Air8101 系)的固件没有包含 usb/usbapp,脚本里这两个名字都是 nil;
  • usbapp.hid_rx 只有 air105 支持,其他平台即使编译了 usbapp,读接口也拿不到数据;
  • 所有函数的 id 参数目前都是占位,实际固定操作设备 0。

要确认手上固件到底带不带,直接跑:

log.info("usbapp", usbapp and "有" or "没有", usb and "usb有" or "usb没有")

两个都没有、又确实需要 HID,只有两条路:换带 usb 库的 160X 固件,或者联系合宙确认当前型号有没有可用的固件版本。

六、常见问题

Q1:start 返回 true,但电脑上没有新设备?

返回值恒为 true,不能作为枚举成功的依据。先确认 hid_mode 是在 start 之前调的,再看有没有 USB_HID_INC 的 READY;数据线要是能传数据的线,供电不足时主机也会拒绝枚举。

Q2:vhid_upload 发出去的字不对、中文变成空格?

这是键盘模式,只能表达 HID 键盘字符集里的可见字符,不支持字符会被替换成空格;要传二进制或任意文本,改用自定义模式(hid_mode(0, 1, 64) + hid_tx),由上位机按原始报文解析。

Q3:U 盘在电脑上能识别但打不开?

多数是卡没先挂到 fatfs,或者文件系统不是主机认的 FAT/exFAT。先用 fatfs.mount 挂上、确认能读写,再 udisk_attach_sdhc;主机弹出前务必先 detach 再 umount。

搜索