用USB HID模拟虚拟电池:Windows免驱实现与固件调试
2026/9/21 1:45:30 网站建设 项目流程

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 Page(0x84):Windows已经内置了完整的解析逻辑。设备只要按照文档上报,系统自动建立电池节点、更新电量图表、响应电源策略。你唯一要做的,是让设备出现在总线上,然后“说”符合规范的数据。

选HID还有一个隐藏红利:HID设备在Windows里天生免驱,因为hidclass.sys是系统组件。无论设备是键盘、鼠标、触摸板还是电池,只要报告描述符声明的是系统标准Usage Page,Windows的hidparse就能“看懂”。这也意味着,这个方案的适用范围不局限于某一颗MCU——STM32、GD32、CH552、ESP32-S3,甚至一颗几块钱的51单片机加USB转HID芯片,理论上都能实现。

1.2 虚拟电池到底“虚拟”了什么

“虚拟电池”这个词容易让人误以为它真的给系统供电。这里必须澄清:本方案中的“电池”只上报状态信息,不传递任何电能。它模拟的是电池管理单元(Fuel Gauge / Battery Management Unit)对外输出的状态信号,包括:

  • 当前电量百分比(RemainingCapacity / FullChargeCapacity)
  • 充电状态(Charging、Discharging、Not Charging)
  • 电池是否存在(Presence / Online)

Windows把这些数据汇总后,会表现出和真实电池一样的行为:任务栏电池图标变化、系统弹出低电量提醒、电源计划在插电/拔电之间切换。对应用程序来说,它调用GetSystemPowerStatus()PowerGetSystemPowerStatus()拿到的数据,和真实电池没有区别。

注意:这只是状态层面的模拟,绝不能通过它给主板、笔记本或任何设备“供电”。它适合的是测试、演示、开发场景,代替真实电池反复充放电带来的机械疲劳和时间成本。

1.3 但Windows到底认不认?

这是很多人第一个疑虑:我插一个USB设备上去,Windows就能识别成电池?答案是可以,但有前提。HID Battery在Windows中不是自动枚举的,设备需要在报告描述符里正确声明电池相关集合(Battery System Collection)。具体来说,报告描述符里必须出现:

  • Usage Page = 0x84(Power Device Page)
  • Usage = 0x02(Battery 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 System:Usage ID 0x02
  • Battery Status:Usage ID 0x10(有的初版文档写作0x03,需要留意,以设备实际枚举结果为准)
  • RemainingCapacity:Usage ID 0x12
  • FullChargeCapacity:Usage ID 0x13
  • Charging / 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: Discharging(0) / Charging(1) // 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侧的处理流程拆开:

  1. USB枚举:设备插上后,USB Host读取设备描述符、配置描述符、接口描述符、HID描述符。HID描述符告诉系统“这个设备的Report Descriptor有多长”。
  2. 报告描述符解析:Windows的hidparse.sys解析报告描述符。当它发现Usage Page = 0x84且Usage = 0x02的集合时,会把这个设备标记为Battery Device。
  3. hidbatt.sys加载:“HID-compliant battery”设备节点被创建,hidbatt.sys作为功能驱动绑定到这个节点。它负责把HID报告的原始字段映射到Windows电源管理接口。
  4. 电源服务接管: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,支持HID,WiFi/BLE可联动功耗偏高需要联网上报的场景
树莓派Pico(RP2040)自带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口,按以下步骤验证:

  1. 打开设备管理器,展开“电池”分类。正常情况下能看到“HID-compliant battery”节点。
  2. 打开任务栏的电池图标,观察电量百分比。如果在模拟放电,图标数值会逐渐下降;如果在充电,会逐渐上升。
  3. 运行powercfg /batteryreport,生成的HTML报告里能看到你的“虚拟电池”信息,包括设计容量、当前容量、循环次数等。这些数据全部来自HID报告。
  4. 在命令行跑一段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 mAh),RemainingCapacity按比例换算。这样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 Hub

4.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(键盘集合 + 电池系统集合),各自使用独立的报告ID,Windows会为同一条物理设备创建多个功能节点。

这个场景在研发通用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电池”插进电脑并被任务栏接纳时,那种对底层协议操控自如的感觉,比调通一个复杂业务代码来得更直接、更扎实。希望这篇文档能帮你少走我之前走过的弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询