先认原生USB口,再给按钮接独立上拉
USB HID从ESP32-S3原生USB-OTG输出,使用GPIO19 D−/J3-20、GPIO20 D+/J3-19;桥USB-to-UART经GPIO43/44做下载与UART0日志,不会替主控发HID。此固件占原生USB,不能同时当另一套USB CDC、USB-Serial/JTAG或其他USB设备栈;这两GPIO也不再外接按钮。
ARM用GPIO5/J1-5,BUTTON用GPIO6/J1-6,各经独立10kΩ上拉3V3,开关/按钮接地LOW有效。GPIO4经330Ω接WP7113ID阳极、阴极共地,仅作本地armed指示。默认ARM开路关闭、按钮松开,全部断电追线后再上电。
固定v1.1官方文档允许两个USB作为同一供电组,本例可两数据线接同一电脑:桥口日志、原生口HID;不再经5V/3V3针注入另一电源。此许可不能外推匿名克隆板。无实际枚举/键盘结果,空CSV等待真实观察。
| 资源 | 用途 | 不要混用 |
|---|---|---|
| GPIO5 / 6 | ARM / BUTTON独立10k上拉,LOW有效 | 不短接两信号 |
| GPIO4 | 330Ω→WP7113ID→GND | 命令指示,不是USB反馈 |
| GPIO19 / 20 | 原生USB D−/D+ | 不再外接GPIO负载 |
| GPIO43 / 44 | 桥UART0 TX/RX 115200 | 桥口不是HID出口 |
参考:[1] 官方固定板:原生USB/桥口/排针/供电组[2] IDF5.5.5 USB Device文档:S3 USB资源[14] Kingbright WP7113ID V.14A
两层官方USB组件都锁到实际解析的版本
锁esp_tinyusb2.2.1、tinyusb0.19.0~3与IDF5.5.5,dependencies.lock随包。官方IDF示例的^2.0.0仅作为API路线参考,没有沿用浮动范围,也没复制自动鼠标/键盘demo。底层版本的~3是注册版本的一部分;裸0.19.0查询失败不等于该精确注册版本不存在。
实际组件分别对应esp-usb提交8e779566ef71d43928cbf7e125e8eb54bab3f542与tinyusb提交ab4a1817907ed865b10a712b5e03e5e0a9902df5,两包CHECKSUMS共2224文件无差异,16个关键官方固定文件与managed逐字一致。使用已有SDK/Python3.12.13,不重复安装整套工具。
HID_COUNT1、CDC关闭、console为桥UART0,SUSPEND与RESUME_CALLBACK必须在最终sdkconfig为y。首轮先解析组件后才补defaults,旧配置仍off,虽构建Exit0也未满足目标;已保留历史并显式改配置实际重编Exit0,最终单任务修正appbin208928B。应用只用组件device_event,不重复定义其强suspend/resume函数。
idf.py --version
python --version
idf.py -B build -D SDKCONFIG=sdkconfig.local build
# 核实际 sdkconfig.local 中两个 CALLBACK=y,再从桥口上传
idf.py -B build -D SDKCONFIG=sdkconfig.local -p /dev/cu.YOUR_BRIDGE_PORT flash monitor参考:[3] IDF5.5.5官方HID示例组件范围(仅参考)[4] esp_tinyusb2.2.1官方注册页[5] TinyUSB0.19.0~3官方注册页[6] 固定esp_tinyusb README:默认描述符与可选回调[7] 固定esp_tinyusb:配置/事件API[8] 固定esp_tinyusb:强mount/suspend/resume回调
只定义一个8字节键盘接口,不自动输入文本
配置描述符34B包含一个HID键盘接口、IN端点0x81、最大包8B、10ms轮询参数,配置宣告100mA且无remote wakeup;100mA是声明,不是测得整板电流。报告描述符65B无reportID,键数组usage/logical最大255,可以表示F13 usage0x68。本站从最终ELF提取并检查字节,未因此声称OS已枚举。
按下报告00 00 68 00 00 00 00 00,释放报告八个0;无宏、无文本、无鼠标。GET_REPORT返回本地命令快照,host键盘LED输出不能使能。报告排队成功或端点再次ready都不是应用动作ACK。
设备字符串Hixinwu Lab / W07 F13 Button (ARM required),固定实验serial W07-LAB-PROTOTYPE不是唯一身份;默认组件实验VID/PID为303A:4004,不代表本站量产VID授权/认证。OS可能缓存相同身份的旧接口,做变更实验时保留具体版本/端口记录。
| 字段 | 值 | 能证明什么 |
|---|---|---|
| HID键 | F13 / 0x68 | 报告可表示单键;实际映射待测 |
| 输入报告 | 8B / 无reportID | 精确按下或全释放字节 |
| 端点 | 0x81 / 8B / 10ms | 描述符参数,非应用时延 |
| USB配置 | 100mA / 无remote wakeup | 声明,不是功耗/唤醒实测 |
参考:[9] 固定描述符控制:自定义FS配置与string[10] 固定默认设备描述符/VID PID算法[11] 固定TinyUSB HID键盘报告宏[12] 固定TinyUSB HID usages:F13=0x68[13] 固定TinyUSB HID ready/report实现
启动按住不发键,恢复后要重新实体使能
TinyUSB任务的SOF事件回调按完整64位时钟节流到至少10ms,ARM/BUTTON各稳定30ms才采纳,调度影响实际边沿。每次启动、重连或组件USB事件,先全释放报告并等端点ready,再接受ARM;ARM从启动就ON不会自动启用,要先OFF稳定再ON,BUTTON也必须先松开。
使能后一次新按下经去抖发F13,本地按下命令到50ms请求ZERO,端点忙可延后,不承诺电脑恰好按住50ms。一直按住不连发;新按下遇端点忙直接丢弃而非缓存,避免恢复时突然发键。ARM关闭后去抖撤使能并请求释放;队列拒绝需要新ARM周期。
USB栈已处理attach/detach/suspend/resume时,在同一USB任务回调同步撤本地按下/使能;GPIO/model tick和tud_hid_report都在该任务SOF回调,事件处理不能跨任务夹在决策与提交之间。ATTACHED重新启用SOF,app_main只初始化后返回。可用SOF回调中检查待发送超过250ms仍未ready则本地取消;SOF停止时等事件或下一次回调再检查,250ms不是离线墙钟保证。下一次ready先ZERO,完整64位处理长运行。
事件处理前已经提交的USB transfer不能靠本地reset撤回,主机恢复后仍可能取走;保证是事件处理后不会按旧armed提交新的PRESS,不是主机绝不会再见到旧按键。桥口仍供电时拔原生线,本电路没有独立VBUS检测,不能保证即时拔线识别;只响应栈已报告状态/事件或待发送超时。线断期间送不出释放,不保证主机清键。只靠原生口供电时拔线为整板断电,重插走启动防护,两组必须分开验收。
- 做到这里,你应该看到
- 能说明本地取消、USB发送释放、主机清除键状态是三件不同的事。
参考:[1] 官方固定板:原生USB/桥口/排针/供电组[8] 固定esp_tinyusb:强mount/suspend/resume回调[13] 固定TinyUSB HID ready/report实现[15] IDF5.5.5完整64位esp_timer时钟
完整主程序:默认关闭、一次按下、一次释放
main.c与下载包逐字一致,hid_model.c/.h和CMake/锁/分区/许可/host工具一并提供。USB事件、SOF内GPIO采样/纯C模型和报告提交由同一TinyUSB任务串行处理,纯C模型统一决定命令;未调用remote wakeup、未等待用户串口打开,也没有开机自发键路径。
GPIO4指示state.armed而非“主机收到”;若要确认动作,必须另看主机键盘事件。失败不会排一个永久重试的按键序列。更换其他usage、布局或USB复合设备已超出本页固定范围,应重新检查描述符和安全启动流程。
/* SPDX-License-Identifier: MIT */
#include <string.h>
#include "esp_log.h"
#include "esp_timer.h"
#include "driver/gpio.h"
#include "tinyusb.h"
#include "tinyusb_default_config.h"
#include "class/hid/hid_device.h"
#include "hid_model.h"
static const char *TAG="W07";
/* After initialization, this state is owned only by the TinyUSB task. */
static w07_state_t state;static bool current_report_pressed;static uint64_t last_poll_ms;static bool polled;
_Static_assert(HID_KEY_F13==0x68,"Locked HID F13 usage must match production model");
const uint8_t w07_report_descriptor[]={TUD_HID_REPORT_DESC_KEYBOARD()};
#define CONFIG_DESC_LEN (TUD_CONFIG_DESC_LEN+TUD_HID_DESC_LEN)
const uint8_t w07_configuration_descriptor[]={
TUD_CONFIG_DESCRIPTOR(1,1,0,CONFIG_DESC_LEN,0,100),
TUD_HID_DESCRIPTOR(0,4,HID_ITF_PROTOCOL_KEYBOARD,sizeof(w07_report_descriptor),0x81,8,10)
};
static const char *strings[]={(const char[]){0x09,0x04},"Hixinwu Lab","W07 F13 Button (ARM required)","W07-LAB-PROTOTYPE","Single keyboard interface"};
uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance){(void)instance;return w07_report_descriptor;}
uint16_t tud_hid_get_report_cb(uint8_t instance,uint8_t report_id,hid_report_type_t type,uint8_t *buffer,uint16_t requested){
(void)instance;if(report_id!=0||type!=HID_REPORT_TYPE_INPUT)return 0;
uint8_t bytes[8];w07_report_bytes(current_report_pressed,bytes);uint16_t n=requested<sizeof(bytes)?requested:sizeof(bytes);memcpy(buffer,bytes,n);return n;
}
void tud_hid_set_report_cb(uint8_t instance,uint8_t report_id,hid_report_type_t type,uint8_t const *buffer,uint16_t size){(void)instance;(void)report_id;(void)type;(void)buffer;(void)size; /* Host keyboard LED output does not arm this device. */}
static void device_event(tinyusb_event_t *event,void *arg){
(void)arg;w07_reset_link(&state);current_report_pressed=false;gpio_set_level(GPIO_NUM_4,0);
/* bus_reset clears TinyUSB's sof_consumer; enable only after configuration. */
if(event->id==TINYUSB_EVENT_ATTACHED)tud_sof_cb_enable(true);
ESP_LOGI(TAG,"USB event=%d; require zero report and fresh physical ARM cycle; submitted transfers cannot be recalled",event->id);
}
/* TinyUSB dispatches SOF and device_event in the same task, not in the ISR.
* No task can process a device_event between model decision and report enqueue.
* Physical/queued events not yet dispatched, and already queued USB transfers,
* are outside that guarantee; a disconnected host cannot be sent a release.
*/
void tud_sof_cb(uint32_t frame_count){
(void)frame_count;const uint64_t now=(uint64_t)esp_timer_get_time()/1000ULL;
if(polled&&now>=last_poll_ms&&now-last_poll_ms<10)return;
polled=true;last_poll_ms=now;
const bool usb=tud_mounted()&&!tud_suspended();const bool ready=tud_hid_ready();
w07_command_t command=w07_tick(&state,now,usb,ready,gpio_get_level(GPIO_NUM_5)==0,gpio_get_level(GPIO_NUM_6)==0);
if(command!=W07_NONE){uint8_t report[8];const bool press=command==W07_PRESS;w07_report_bytes(press,report);bool accepted=tud_hid_report(0,report,sizeof(report));
if(accepted){w07_queued(&state,command,now);current_report_pressed=press;}else{w07_failed(&state);current_report_pressed=false;}
ESP_LOGI(TAG,"REPORT %s queued=%u; USB queue acceptance is not application effect",press?"F13_PRESS":"ALL_RELEASED",accepted);
}
gpio_set_level(GPIO_NUM_4,state.armed);
}
void app_main(void){
gpio_config_t inputs={.pin_bit_mask=(1ULL<<5)|(1ULL<<6),.mode=GPIO_MODE_INPUT};ESP_ERROR_CHECK(gpio_config(&inputs));
gpio_config_t led={.pin_bit_mask=1ULL<<4,.mode=GPIO_MODE_OUTPUT};ESP_ERROR_CHECK(gpio_config(&led));gpio_set_level(GPIO_NUM_4,0);
w07_reset_link(&state); /* Complete before driver creates the USB task. */
tinyusb_config_t cfg=TINYUSB_DEFAULT_CONFIG(device_event);cfg.descriptor.string=strings;cfg.descriptor.string_count=sizeof(strings)/sizeof(strings[0]);cfg.descriptor.full_speed_config=w07_configuration_descriptor;
esp_err_t e=tinyusb_driver_install(&cfg);if(e!=ESP_OK){ESP_LOGE(TAG,"TinyUSB install=%s; no key reports",esp_err_to_name(e));return;}
ESP_LOGI(TAG,"ARM GPIO5 and BUTTON GPIO6 activeLOW external10k; nativeUSB19/20, UART0 bridge; USB task owns polling/reports; no automatic key at boot");
}
参考:[6] 固定esp_tinyusb README:默认描述符与可选回调[7] 固定esp_tinyusb:配置/事件API[8] 固定esp_tinyusb:强mount/suspend/resume回调[11] 固定TinyUSB HID键盘报告宏[12] 固定TinyUSB HID usages:F13=0x68[13] 固定TinyUSB HID ready/report实现[15] IDF5.5.5完整64位esp_timer时钟
在安全观察窗口逐项做枚举、按下、keyup和拔插
尚未执行真实电脑键盘实验。先ARM关、按钮松开,桥口刷入并保留日志,再把原生USB接同一电脑。OS USB设备树/设备管理器要确认产品字符串、303A:4004和键盘接口,不能以出现一个串口作为HID成功依据。
包内host/key-observer.html仅监听有焦点时isTrusted键盘事件,不访问USB、不注入按键;用本地http.server打开。F13可能被系统或快捷键工具截获,未观察到事件先核焦点/系统映射,不改成会在编辑器输入文本的自动宏。操作前移开终端/编辑器/远程会话等敏感输入焦点。
- 工程根运行python -m http.server 8766 --bind 127.0.0.1 --directory host,浏览器打开127.0.0.1:8766/key-observer.html并点击观察按钮。
- ARM关稳定→开稳定,按一次BUTTON;分别记录UART PRESS/ZERO与实际F13 keydown/keyup,不把queued行当浏览器事件。
- 保持BUTTON10秒检查不自动重复,松开再按检查下一组;另做快速抖动、ARM关闭按键、启动ARM已开、启动BUTTON已按住。
- 按下期间ARM关;分开做电脑挂起/恢复、桥口供电下拔原生线、仅原生供电断电重插,记录实际USB事件与主机键状态。
- 重连先ZERO且无自动按键,重新OFF→ON才试下一键;结束关ARM、拔原生线,所有失败行保留。
参考:[1] 官方固定板:原生USB/桥口/排针/供电组[8] 固定esp_tinyusb:强mount/suspend/resume回调[13] 固定TinyUSB HID ready/report实现
原生口被占用时,桥UART0仍是恢复入口
只插桥口能下载却无HID是预期,检查第二根原生数据线和接口。旧固件占用USB或枚举失败时,不依赖已经不可用的HID口救援:ARM关、拔原生线、桥口供电,按住BOOT、点RESET、松BOOT,经桥口刷回本例或已知良好固件。USB供电组不再叠针脚输入。
启动/重连不发键先核ZERO首报、ARM fresh cycle与按钮先松开,这是保护条件。若真实观察出现残留键,先取消ARM/拔HID,必要时用实体键盘释放或重开观察应用,记录故障;固件不能隔空送释放到已断线主机。
组件callbacks默认更改后无事件,先查实际sdkconfig而非只读defaults;本例最终两个y,应用不定义重复强回调。无法及时获得端点ready的按键不是等网络恢复后补发,用户需要明确重新使能和操作。
| 现象 | 检查 | 恢复 |
|---|---|---|
| 有串口无HID | 桥/原生口与数据线 | ARM关后确认原生口/OS设备树 |
| 旧USB固件阻塞 | USB资源占用/日志口 | 桥BOOT+RESET刷回 |
| 恢复不发键 | ZERO、ARM off/on、BUTTON已松开 | 按保护顺序重做 |
| 失联/缺keyup | 真实主机事件与USB状态 | 取消ARM/断HID,保留故障,不承诺远端释放 |
参考:[1] 官方固定板:原生USB/桥口/排针/供电组[2] IDF5.5.5 USB Device文档:S3 USB资源[6] 固定esp_tinyusb README:默认描述符与可选回调[8] 固定esp_tinyusb:强mount/suspend/resume回调
同源状态机与ELF检查都完成,物理USB仍待验收
本轮同源ASan/UBSan夹具通过启动按住、30ms稳定、实体使能、一次F13/50ms释放意图、长按不重复、ARM关闭、忙端点丢按键、队列失败、重连先零、250ms及完整64位时间。另用显式host USB/GPIO桩编译实际main.c,按真实回调顺序注入SUSPEND+RESUME/重连,核已处理事件后同步撤ARM、先ZERO和新实体周期;占位描述符宏不替代ELF检查。它不运行真实TinyUSB栈、不枚举USB,也不向电脑产生真实键盘事件。
最终ELF内65B/34B描述符检查留在ble-usb-hid-descriptors.json,实际组件解析、首轮callbacks-off与最终callbacks-on构建日志/配置/产物SHA分开记录。空observations.csv保留USB枚举、原生拔插、启动按住、挂起/恢复和主机键盘结果的待测位置。全部物理项仍pending,不能把“编译通过”改成“已能控制电脑”。
参考:[6] 固定esp_tinyusb README:默认描述符与可选回调[8] 固定esp_tinyusb:强mount/suspend/resume回调[11] 固定TinyUSB HID键盘报告宏[13] 固定TinyUSB HID ready/report实现
资料来源与版本说明
按固定官方实现、厂家引脚和实际构建核查;主机夹具仅证明软件边界,指定板/手机/USB实体运行仍未执行。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。
- 官方固定板:原生USB/桥口/排针/供电组
- IDF5.5.5 USB Device文档:S3 USB资源
- IDF5.5.5官方HID示例组件范围(仅参考)
- esp_tinyusb2.2.1官方注册页
- TinyUSB0.19.0~3官方注册页
- 固定esp_tinyusb README:默认描述符与可选回调
- 固定esp_tinyusb:配置/事件API
- 固定esp_tinyusb:强mount/suspend/resume回调
- 固定描述符控制:自定义FS配置与string
- 固定默认设备描述符/VID PID算法
- 固定TinyUSB HID键盘报告宏
- 固定TinyUSB HID usages:F13=0x68
- 固定TinyUSB HID ready/report实现
- Kingbright WP7113ID V.14A
- IDF5.5.5完整64位esp_timer时钟