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。