ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

用USB HID模拟虚拟电池:Windows免驱实现与固件调试

用USB HID模拟虚拟电池:Windows免驱实现与固件调试 1. 方案思路为什么能用USB HID模拟出一块“虚拟电池”先把这个方案的本质说清楚**Windows系统支持通过USB HID协议上报电源状态系统会把它识别为一颗“HID Battery”并纳入电源管理逻辑整个过程不需要厂商驱动不需要管理员权限安装任何东西。**换句话说只要你的MCU上报正确的HID报告Windows就会老老实实地相信“这块电池还有85%的电量正在充电”。这个思路我第一次意识到可行是在调试低功耗IoT设备固件时被逼出来的。当时需要反复测试设备在“低电量保护”逻辑下的行为但手里只有一个电池供电的样机每次把电量耗尽再充电一个循环就要等几个小时测试效率低得让人抓狂。后来我仔细翻了Windows的ACPI和HID相关文档发现系统原生支持一类叫“HID Battery”的设备它和键盘、鼠标一样走的是USB HID通道根本不需要单独的驱动——这也就成了整个方案的基石。1.1 为什么选USB HID而不是厂商自定义通道做Windows下设备通信工程师传统上第一反应是写个驱动或者至少做个HID厂商自定义用法页Vendor-Defined Usage Page。但这两条的开发成本和部署成本都不低内核驱动需要签名的驱动包64位系统强制驱动签名开发机要开测试模式部署到客户机器上更麻烦。只是为了一件调试工具做一套签名驱动性价比太低。厂商自定义HID虽然免驱但Windows不会对vendor page的数据做任何解读所有数据都要自己在应用层解析等于通信协议和应用层逻辑都得自己写。标准电源Usage Page0x84Windows已经内置了完整的解析逻辑。设备只要按照文档上报系统自动建立电池节点、更新电量图表、响应电源策略。你唯一要做的是让设备出现在总线上然后“说”符合规范的数据。选HID还有一个隐藏红利HID设备在Windows里天生免驱因为hidclass.sys是系统组件。无论设备是键盘、鼠标、触摸板还是电池只要报告描述符声明的是系统标准Usage PageWindows的hidparse就能“看懂”。这也意味着这个方案的适用范围不局限于某一颗MCU——STM32、GD32、CH552、ESP32-S3甚至一颗几块钱的51单片机加USB转HID芯片理论上都能实现。1.2 虚拟电池到底“虚拟”了什么“虚拟电池”这个词容易让人误以为它真的给系统供电。这里必须澄清本方案中的“电池”只上报状态信息不传递任何电能。它模拟的是电池管理单元Fuel Gauge / Battery Management Unit对外输出的状态信号包括当前电量百分比RemainingCapacity / FullChargeCapacity充电状态Charging、Discharging、Not Charging电池是否存在Presence / OnlineWindows把这些数据汇总后会表现出和真实电池一样的行为任务栏电池图标变化、系统弹出低电量提醒、电源计划在插电/拔电之间切换。对应用程序来说它调用GetSystemPowerStatus()或PowerGetSystemPowerStatus()拿到的数据和真实电池没有区别。注意这只是状态层面的模拟绝不能通过它给主板、笔记本或任何设备“供电”。它适合的是测试、演示、开发场景代替真实电池反复充放电带来的机械疲劳和时间成本。1.3 但Windows到底认不认这是很多人第一个疑虑我插一个USB设备上去Windows就能识别成电池答案是可以但有前提。HID Battery在Windows中不是自动枚举的设备需要在报告描述符里正确声明电池相关集合Battery System Collection。具体来说报告描述符里必须出现Usage Page 0x84Power Device PageUsage 0x02Battery System集合内包含电池状态Battery Status、电量百分比RemainingCapacity可选的还有满充容量、设计容量、充电电流电压等只要报告描述符里声明了这些集合Windows的hidclass会创建一个功能设备然后交给hidbatt.sys驱动处理。hidbatt.sys是Windows自带的HID电池驱动不需要额外安装。设备管理器里会多出一个“电池”节点里面能看到“电源状态”选项卡。我最早验证这个方案时用STM32F103 板载USB端口烧了一个最小固件插上电脑设备管理器里立刻出现了“HID-compliant battery”任务栏电量图标也从无到有。那一刻我意识到这条路完全走得通。2. 核心细节解析报告描述符和报告体是整套方案的心脏2.1 报告描述符怎么声明电池集合报告描述符是整个HID设备身份的“身份证”系统靠它判断设备是什么、能干什么。描述符里电池相关的关键字段如下// 电池系统集合的声明Power Device Usage Page 0x05, 0x84, // Usage Page (Power Device) 0x09, 0x02, // Usage (Battery System) 0xA1, 0x01, // Collection (Application) // ------ BatteryStatus 电池状态 ------ 0x05, 0x84, // Usage Page (Power Device) 0x09, 0x10, // Usage (Battery Status)????这里我要特别说明一点网上不少资料直接照抄HID Usage Tables里的Usage ID但不同版本的文档对电池状态Battery Status和支持状态Battery Support Status的ID定义略有差异。我在实际调试中踩过坑最后使用的是HID Usage Tables 1.3中明确的值Battery SystemUsage ID 0x02Battery StatusUsage ID 0x10有的初版文档写作0x03需要留意以设备实际枚举结果为准RemainingCapacityUsage ID 0x12FullChargeCapacityUsage ID 0x13Charging / Discharging状态通过Battery Status的feature report的位标志上报这个集合我在报告描述符里对于“Battery Status”特别设计了5个字节的控制位。第1个字节用于定义电池是否在校准、是否处于充电、是否放电、是否故障第2个字节用于表示是否需要充电第3个字节是电池是否存在Presence第4、5字节保留。Windows的hidbatt.sys拿到这些值后会转换成系统内部的电池属性再触发任务栏图标的更新。提示电池状态Battery Status的每一位定义都可以在USB-IF的HID Usage Tables里查到建议以官方PDF为准不要只凭记忆因为这个细节太容易出错了。2.2 为什么还需要一个“Feature Report”这里有个关键点HID设备上报数据可以用Input Report设备→主机但电池状态这种“主机主动查询拖拽”的信息用Feature Report更合适。因为Windows电源服务可能会主动向设备发送查询请求而Feature Report天然支持双向通信——主机可以Get/Set。我设计的报告结构是Input Report设备→主机默认推送包含RemainingCapacity百分比和Battery Status全量字段设备每次数值变化时主动上报。Feature Report双向承载校准Calibration命令、满充容量Full Charge Capacity读写用于工程调试时调整“虚拟电池”的行为参数。这么拆的好处是明显的系统默认能拿到实时电量Input Report而工程师需要临时改满充容量、模拟电池老化时直接通过Windows API下发一个Feature Report命令就行不用重新编译固件。2.3 报告体Report Body的具体布局下面是实际固件中使用的Feature Report布局字段说明和偏移量我标注在注释里// Feature Report: 长度为8字节 // Byte 0: 报告ID固定为0x03 // Byte 1: Battery Status状态位 // Bit 0: Discharging0 / Charging1 // Bit 1: Critical Level // Bit 2: Low Level // Bit 3: Need Maintenance // Bit 4: Failed // Byte 2: Presence, 0x01表示电池在位 // Byte 3-4: RemainingCapacity百分比, 小端序, 范围0-100 // Byte 5-6: FullChargeCapacity, 用于校准比例 // Byte 7: 保留为什么RemainingCapacity用两个字节而不是一个字节因为HID报告的容量属性允许以mAh为单位范围可能超过255两个字节能覆盖0-65535的计数范围。而百分比情况即使一个字节够用双字节也让字段对齐更清晰而且还能直接映射到Windows内部的BatteryCapacity结构。Input Report就简单多了// Input Report: 长度为5字节 // Byte 0: 报告ID, 固定为0x02 // Byte 1: Battery Status // Byte 2-3: RemainingCapacity百分比 // Byte 4: Online状态, 0x01在线我实测下来Windows对Input Report的接收频率并不敏感但建议不要低于每秒一次。太低了任务栏电量图标会有滞后感太高了浪费USB带宽完全没有必要。2.4 枚举过程设备插上去之后Windows做了什么为了让你彻底掌握排查思路这里把Windows侧的处理流程拆开USB枚举设备插上后USB Host读取设备描述符、配置描述符、接口描述符、HID描述符。HID描述符告诉系统“这个设备的Report Descriptor有多长”。报告描述符解析Windows的hidparse.sys解析报告描述符。当它发现Usage Page 0x84且Usage 0x02的集合时会把这个设备标记为Battery Device。hidbatt.sys加载“HID-compliant battery”设备节点被创建hidbatt.sys作为功能驱动绑定到这个节点。它负责把HID报告的原始字段映射到Windows电源管理接口。电源服务接管Windows的power服务调用NtDeviceIoControlFile接口和hidbatt.sys通信读取电池状态。最终在任务栏气泡、powercfg /batteryreport等处体现。这个过程全程不需要第三方驱动参与。所以“零驱动”不是噱头而是系统原生机制的合理利用。3. 实操过程从零开始搭一个USB HID虚拟电池3.1 硬件选型一颗带USB的MCU就够实测下来以下几类硬件都适合做这个方案硬件平台优点缺点适合场景STM32F103/GD32F103USB FS外设成熟资料多需要晶振和外围电路原型验证CH552系列内置USB便宜封装小Flash/RAM小需要专用工具链量产小工具ESP32-S3内置USB-OTG支持HIDWiFi/BLE可联动功耗偏高需要联网上报的场景树莓派PicoRP2040自带USB控制器MicroPython可写USB栈不够底层需自研快速验证我自己用STM32F103C8T6做的最小系统板验证的原因是手头就有一块。接线只有USB D、D-和电源地三路非常简单。3.2 固件工程结构HID描述符和端点配置STM32的USB库需要配置端点描述符。我使用的是标准4端点方案控制端点0、中断输入端点1用于Input Report、中断输出端点2用于Output Report本方案没用到但保留方便扩展。以下是关键初始化代码// USB HID配置结构体 static const HID_CONFIG_STRUCT HID_Config { .HID_ReportDesc (uint8_t *)HID_ReportDescriptor, .HID_ReportDescLen sizeof(HID_ReportDescriptor), .HID_InputReportLen 5, // Input Report 5字节 .HID_FeatureReportLen 8, // Feature Report 8字节 .HID_EPin 0x81, // 端点1方向IN .HID_EPinSize 64, // 全速HID端点最大包长64字节 .HID_EPinInterval 10, // 轮询间隔10ms };一点说明端点包长虽然是64字节但实际报告只有5字节其余的空间会被填充0。这是USB HID很常见的做法不影响功能。3.3 电量模拟的核心逻辑虚拟电量的核心状态机其实很简单我用一个定时器每1秒更新一次“虚拟电量”然后发布到Input Report。逻辑如下// 虚拟电量状态机 typedef struct { unsigned int capacity; // 当前电量百分比 0-100 unsigned char isCharging; // 是否处于充电状态 unsigned char isOnline; // 电池是否在线 unsigned int cycleCount; // 累计充放电循环次数 } VirtualBattery; #define BATTERY_DEFAULT_CAPACITY 85 #define BATTERY_DISCHARGE_STEP 1 // 每次-1% #define BATTERY_CHARGE_STEP 2 // 每次2% void Battery_Task(void) { if (battery.isOnline) { if (battery.isCharging) { battery.capacity BATTERY_CHARGE_STEP; if (battery.capacity 100) { battery.capacity 100; battery.isCharging 0; // 充满后自动切换为放电 } } else { if (battery.capacity 0) { battery.capacity - BATTERY_DISCHARGE_STEP; } else { battery.capacity 0; battery.isCharging 1; // 电量耗尽后自动进入充电态 } } } }这里要特别提醒Windows对电池状态的“变化”非常敏感。如果设备上报的剩余容量长期不变系统可能认为电池“卡住”了甚至会显示“电源已接通未充电”。所以即使是测试也要让状态机持续运转——要么走充电曲线要么走放电曲线不要长时间停在静止状态。3.4 Windows侧如何验证虚拟电池生效固件烧录后插上USB口按以下步骤验证打开设备管理器展开“电池”分类。正常情况下能看到“HID-compliant battery”节点。打开任务栏的电池图标观察电量百分比。如果在模拟放电图标数值会逐渐下降如果在充电会逐渐上升。运行powercfg /batteryreport生成的HTML报告里能看到你的“虚拟电池”信息包括设计容量、当前容量、循环次数等。这些数据全部来自HID报告。在命令行跑一段PowerShell脚本直接读取电量Get-WmiObject -Namespace root\wmi -Class BatteryStatus | Select -ExpandProperty EstimatedChargeRemaining如果这个返回值和你固件里设置的容量一致说明整条链路全部打通了。实际测试中任务栏图标从无到有的那一刻非常有意思——系统完全没有意识到“这是一颗假的电池”它只是信任USB设备上报的数据。这也是HID协议作为一种通用输入/输出协议设计得非常优雅的地方。3.5 调参经验让电量曲线更真实单纯做线性加减电量太粗暴真实电池的充放电曲线不是一条直线。特别是低电量段20%以下真实电池电压会快速跌落对应电量变化会加速而充电时80%以后会出现涓流、恒压阶段充电速度明显变慢。为了让虚拟电池看起来更像真的我建议在固件里内置一组“查表式”充放电曲线容量区间放电速率充电速率80%-100%每2秒-1%每5秒1%30%-80%每1秒-1%每3秒1%0%-30%每200ms-1%每2秒1%把这张表用常数数组写死在固件里每次定时器更新时根据当前容量查表决定增减速度。这样Windows上看到的电量曲线带有明显的“非线性”和真实电池的体验非常接近。这个细节在演示项目时特别重要——投资人或者领导围观时如果电量刷刷地直线跌一眼就会被看穿不是真电池。4. 常见问题与排查技巧实录4.1 设备插上后设备管理器里没有“HID-compliant battery”节点用USB抓包工具Wireshark USBPcap或Ellisys抓一下枚举过程重点看设备描述符里bcdHID版本和协议版本。很多情况下是报告描述符写错了导致hidbatt.sys拒绝绑定。最直接的排查办法是先把这个设备的状态栏里看一下“HID-compliant battery”不出现是否有“未知USB设备”提示。常见的原因有三个报告描述符里Usage Page写成了Vendor定义的0xFF00系统只当它是普通厂商设备。Battery System集合后面少了End Collection报告描述符没有正确结束导致解析失败。Input Report和Feature Report长度和报告描述符里声明的不一致Windows加载hidbatt.sys时发现字段对不上直接拒绝加载功能驱动。排查时我习惯用HID报告描述符分析工具网上有现成的v1.7小工具把固件中的报告描述符贴进去看它解析出来的usage tree是不是符合预期。这个工具特别适合快速确认Description里“Battery System”字样是否出现。4.2 电池节点出现了但电量一直显示0%这个问题说明hidbatt.sys已经加载但拿到的Raw Capacity值是0。原因通常出在RemainingCapacity字段格式上——Windows要求这个字段的值和FullChargeCapacity字段的比值来计算百分比。如果你只上报了RemainingCapacity没有上报FullChargeCapacity或者FullChargeCapacity字段一次报告中没有同时给出系统会认为电量数据无效。解决办法Feature Report中同时上报两个字段而且确保RemainingCapacity ≤ FullChargeCapacity。我一般会设计成动态校准每次修改电量时FullChargeCapacity保持恒定比如7200 mAhRemainingCapacity按比例换算。这样Windows能计算出准确百分比。4.3 任务栏电池图标不更新这通常是设备上报频率太低或者没有上报事件。Windows的电源服务默认有一个去抖机制如果一段事件内数据没有变化服务就默认电池维持原状。如果你希望“电量下降”的效果能实时反映在任务栏上需要保证至少1秒上报一次Input Report且容量值每次都不同。另外有的场景下Windows会自动把电池状态切换到“不插入电源”并显示一颗没有活动量的电池这会给人“图标卡死”的错觉。这时去设备管理器禁用再启用一次“HID-compliant battery”节点图标就会强制刷新。4.4 与ACPI设备冲突虚拟电池和真电池同时存在如果你的开发设备本身有真实电池比如笔记本插上USB虚拟电池后Windows会显示两块电池。这在开发和测试场景中是可以接受的但如果要验证“只有虚拟电池”的系统状态可以在BIOS中临时禁用内置电池或者在物理机上拆掉电池连线。后者风险较高建议仅限开发样机。如果不想拆电池也可以在设备管理器里禁用真实电池节点模拟出“只有USB设备供电”的场景。4.5 低功耗/休眠时USB口断电有些笔记本的USB口在休眠时会断电导致虚拟电池消失。这不是固件问题而是硬件电源管理的策略。如果你要在待机状态下测试电池行为建议用一个外部供电的USB Hub或者使用一个带外部供电的USB隔离器保证USB口电源稳定。用一张表格总结一下上述排查点现象可能原因排查方向无电池节点报告描述符未声明Power Device集合检查Usage Page / Usage ID电量固定0%FullChargeCapacity缺失或为0确保Feature Report两个容量字段都在图标不更新上报频率低 / 容量值长期不变提高频率让状态机持续走充放电曲线双电池图标真电池和虚拟电池共存禁用或物理拆掉真电池休眠后丢失USB口休眠断电使用外部供电USB Hub4.6 踩坑HID报告ID和Feature Report的隐藏问题很多人在做HID设备时习惯性只定义一个Input Report没有单独定义Feature Report。问题是Windows的hidbatt.sys有可能会试图通过Feature Report来获取或修改电池状态如果设备不支持Feature Report或者Feature Report的长度和系统预期不符驱动加载过程会失败表现为设备节点带黄色感叹号。所以在报告描述符里必须至少包含一个Feature Report并声明Battery Status字段。哪怕你不需要主机下发命令也必须声明这个Feature Report来“占位”。另外如果多个报告ID记得HID描述符里要声明独立集合并且在端点包缓冲区里给不同报告ID预留足够空间。5. 扩展应用把“虚拟电池”变成你的开发基础设施5.1 电池保护逻辑的自动化测试低功耗产品NB-IoT模块、数字温湿度计、烟雾报警器、定位器内部通常有电池管理逻辑电量低于20%进入低功耗Wi-Fi扫描策略低于10%会强制开启Sensor Hub状态采集低于5%直接停机。这些逻辑在开发调试阶段很难反复用真实电池验证因为真实电池充满再放完太耗时了。用虚拟电池你可以写一个自动化脚本通过HID Feature Report的下发直接把虚拟电量设置在任意百分比// 通过Feature Report设置电池容量 static void SetVirtualCapacity(uint16_t percent) { uint8_t buf[8]; buf[0] 0x03; // Report ID buf[1] 0x01; // Status: 在位 buf[2] 0x01; // Presence buf[3] (uint8_t)(percent 0xFF); buf[4] (uint8_t)((percent 8) 0xFF); buf[5] (uint8_t)(FULL_CHARGE_CAPACITY 0xFF); buf[6] (uint8_t)((FULL_CHARGE_CAPACITY 8) 0xFF); buf[7] 0x00; HID_FeatureSend(buf, 8); }主机端可以写个C#或Python脚本每100ms下发一次Feature Report把电量从100%线性拉低到0%同时自动化去抓设备日志、截图、记录关键事件。整个电池保护逻辑的全覆盖测试在一个小时内就能全部跑完。5.2 产品演示和UI联调工具很多产品发布会、展会或领导验收时需要演示“低电量提醒”这一交互过程。但真实设备现场很难保证恰好处在低电量状态。用虚拟电池方案你可以现场通过一根USB线“遥控”电量值。展示完正常状态后直接下发5%电量弹窗提醒立即出现不需要任何等待。硬件方面只要在设备内部预留一个USB口和一个拨码开关或者直接通过无线方式ESP32-S3方案远程控制电量就是一台极好的演示机器。5.3 测试Windows系统集成逻辑Windows上有些软件会根据电池状态做出不同响应节电模式、屏幕亮度自动调节、软件功能限制等。比如游戏本会在插电时解锁高功耗模式拔电时自动降频。如果产品团队想验证这些功能的Windows适配情况用虚拟电池可以在不插拔真电源的情况下动态切换“在线/离线”状态测试效率提升非常明显。5.4 结合HID设备做“复合设备”一个USB设备可以同时声明键盘、鼠标、电池三个集合。这意味着你可以做一个设备既是键盘又上报电池状态。在实现时报告描述符里包含多个Collection键盘集合 电池系统集合各自使用独立的报告IDWindows会为同一条物理设备创建多个功能节点。这个场景在研发通用HID调试器时会很有价值——一个USB口既负责发送按键指令测试电脑的快捷键逻辑又能实时回报虚拟电量给上层应用做联动。6. 一个真实的调试案例让“假电池”在Win11上正常显示最后分享一个我在Win11上调试时遇到的具体案例算是给整个流程做一个收官说明。当时固件烧进去后设备管理器里能看到未知设备但“电池”分类下始终不出现HID-compliant battery。常规报表描述符分析工具检查也没有发现问题——Usage Page 0x84、Usage ID 0x02、Battery System集合都在。后来我用HID抓包工具抓了枚举过程发现Input Report的Report ID被定义成了0x01但Feature Report的Report ID是0x03。问题在于hidbatt.sys会尝试获取Feature Report来读取电池状态而Feature Report里Report ID和Input Report的定义必须符合报告描述符的声明顺序。我交换了Feature Report和Input Report在报告描述符中的声明顺序后再插入设备Windows立刻识别成功。这个问题背后的机制我后来才完全弄明白hidbatt.sys的字段绑定基于Report ID在描述符中出现的顺序。它默认把第一次遇到的Battery Status字段当作输入数据源后面遇到的Feature Report字段作为参数源。声明顺序颠倒会导致字段绑定错位驱动因此拒绝加载。给个结论性经验在报告描述符里先声明Feature Report集合再声明Input Report集合可以让Windows的hidbatt.sys更稳定地绑定电池属性。这个顺序我在所有后续项目中都固定下来了。做这个项目最大的收获是很多看似需要厂商驱动支持的设备类型Windows用标准HID协议就能覆盖只是需要你对报告描述符的理解到达“逐位可控”的层面。当你亲手把一个“USB电池”插进电脑并被任务栏接纳时那种对底层协议操控自如的感觉比调通一个复杂业务代码来得更直接、更扎实。希望这篇文档能帮你少走我之前走过的弯路。
返回列表