前面的 PVD(Physical Virtual Device)设计过普通鼠标,绝对值鼠标, 这次带来的是一个虚拟电池的设计。在进行功耗和性能测试的时候,电池状态(AC/DC)对于Windows性能释放有着很大的影响。因此需要有手段来虚拟电池,之前我设计过2款虚拟电池软件的,但是这种软件是通过驱动来实现的,在具体使用时会有很大局限性。
这次带来的是使用 CH554模拟的USB HID 设备,它将自身报告为一个 UPS 设备,然后通过 USB 接口将当前电池信息报告给 Windows。代码是 Arduino 写成的,通俗易懂,只需要有 USB 知识就可以掌握。整体框架来自另外一个基于 Leonardo 的Arduino 项目。
硬件部分非常简单,就是一个 CH554e的最小系统(MSOP10)封装,非常适于制作小型设备。
#ifndef USER_USB_RAM
#error "This example needs to be compiled with a USER USB setting"
#endif
#include <WS2812.h>
#include "src/CdcHidCombo/USBCDC.h"
#include "src/CdcHidCombo/USBHIDKeyboardMouse.h"
#include "src/CdcHidCombo/PowerDevice.h"
#include "src/CdcHidCombo/USBconstant.h"
#define NUM_LEDS 1
#define COLOR_PER_LEDS 3
#define NUM_BYTES (NUM_LEDS*COLOR_PER_LEDS)
__xdata uint8_t ledData[NUM_BYTES];
#define MINUPDATEINTERVAL 26000UL
#define OnBoardLED 0x03
const byte bDeviceChemistry = IDEVICECHEMISTRY;
const byte bOEMVendor = IOEMVENDOR;
uint16_t iPresentStatus = 0, iPreviousStatus = 0;
byte bRechargable = 1;
byte bCapacityMode = 0; // units are in mWh
// Physical parameters
const uint16_t iConfigVoltage = 1380;
uint16_t iVoltage = 1300, iPrevVoltage = 0;
uint16_t iRunTimeToEmpty = 0, iPrevRunTimeToEmpty = 0;
uint16_t iAvgTimeToFull = 7200;
uint16_t iAvgTimeToEmpty = 7200;
uint16_t iRemainTimeLimit = 600;
int16_t iDelayBe4Reboot = -1;
int16_t iDelayBe4ShutDown = -1;
byte iAudibleAlarmCtrl = 2; // 1 - Disabled, 2 - Enabled, 3 - Muted
// Parameters for ACPI compliancy
uint8_t iDesignCapacity = 0xFF;
byte iWarnCapacityLimit = 10; // warning at 10%
byte iRemnCapacityLimit = 5; // low at 5%
const byte bCapacityGranularity1 = 1;
const byte bCapacityGranularity2 = 1;
uint8_t iFullChargeCapacity = 0xFF;
uint8_t iRemaining = 0xFF, iPrevRemaining = 0;
int iRes = 0;
unsigned long iIntTimer=0;
// Data format
// Keyboard(Total 9 bytes): 01(ReportID 01) + Keyboard data (8 Bytes)
// Mouse(Total 5 bytes): 02(ReportID 02) + Mouse Data (4 Bytes)
uint8_t recvStr[9];
uint8_t recvStrPtr = 0;
unsigned long Elsp;
uint8_t FeatureBuffer[256];
FeatureType FeatureList[32];
uint8_t FeatureRecord = 0;
uint16_t iManufacturerDate = 0,bCycles=20;
void setFeature(uint8_t id, uint8_t* Data, int Len)
{
/*
Serial0_print("ID:");
Serial0_print(id);
Serial0_print_c(' ');
Serial0_print(Data[0]);
Serial0_print_c(' ');
if (Len>1) {
Serial0_print(Data[1]);
Serial0_print_c(' ');
}
Serial0_print(Len);
Serial0_println_c(' ');
*/
FeatureList[id].Index = FeatureRecord;
FeatureList[id].Size = Len;
for (uint8_t i = 0; i < Len; i++) {
FeatureBuffer[FeatureRecord] = Data[i];
FeatureRecord++;
}
}
void setup() {
Serial0_begin(500000);
delay(1000);
Serial0_println("st");
uint8_t strIndex;
strIndex = 5;
setFeature(HID_PD_IPRODUCT, &strIndex, sizeof(strIndex));
strIndex = 6;
setFeature(HID_PD_MANUFACTURER, &strIndex, sizeof(strIndex));
strIndex = 7;
setFeature(HID_PD_SERIAL, &strIndex, sizeof(strIndex));
strIndex = 8;
setFeature(HID_PD_IDEVICECHEMISTRY, &strIndex, sizeof(strIndex));
setFeature(HID_PD_PRESENTSTATUS, &iPresentStatus, sizeof(iPresentStatus));
setFeature(HID_PD_RUNTIMETOEMPTY, &iRunTimeToEmpty, sizeof(iRunTimeToEmpty));
setFeature(HID_PD_AVERAGETIME2FULL, &iAvgTimeToFull, sizeof(iAvgTimeToFull));
setFeature(HID_PD_AVERAGETIME2EMPTY, &iAvgTimeToEmpty, sizeof(iAvgTimeToEmpty));
setFeature(HID_PD_REMAINTIMELIMIT, &iRemainTimeLimit, sizeof(iRemainTimeLimit));
setFeature(HID_PD_DELAYBE4REBOOT, &iDelayBe4Reboot, sizeof(iDelayBe4Reboot));
setFeature(HID_PD_DELAYBE4SHUTDOWN, &iDelayBe4ShutDown, sizeof(iDelayBe4ShutDown));
setFeature(HID_PD_RECHARGEABLE, &bRechargable, sizeof(bRechargable));
setFeature(HID_PD_CAPACITYMODE, &bCapacityMode, sizeof(bCapacityMode));
setFeature(HID_PD_CONFIGVOLTAGE, &iConfigVoltage, sizeof(iConfigVoltage));
setFeature(HID_PD_VOLTAGE, &iVoltage, sizeof(iVoltage));
setFeature(HID_PD_IOEMINFORMATION, &bOEMVendor, sizeof(bOEMVendor));
setFeature(HID_PD_AUDIBLEALARMCTRL, &iAudibleAlarmCtrl, sizeof(iAudibleAlarmCtrl));
setFeature(HID_PD_DESIGNCAPACITY, &iDesignCapacity, sizeof(iDesignCapacity));
setFeature(HID_PD_FULLCHRGECAPACITY, &iFullChargeCapacity, sizeof(iFullChargeCapacity));
setFeature(HID_PD_REMAININGCAPACITY, &iRemaining, sizeof(iRemaining));
setFeature(HID_PD_WARNCAPACITYLIMIT, &iWarnCapacityLimit, sizeof(iWarnCapacityLimit));
setFeature(HID_PD_REMNCAPACITYLIMIT, &iRemnCapacityLimit, sizeof(iRemnCapacityLimit));
setFeature(HID_PD_CPCTYGRANULARITY1, &bCapacityGranularity1, sizeof(bCapacityGranularity1));
setFeature(HID_PD_CPCTYGRANULARITY2, &bCapacityGranularity2, sizeof(bCapacityGranularity2));
setFeature(HID_PD_CYCLECOUNT,&bCycles,sizeof(bCycles));
setFeature(HID_PD_CONFIGVOLTAGE, &iConfigVoltage, sizeof(iConfigVoltage));
iManufacturerDate = (2025 - 1980) * 512 + 1 * 32 + 1;
setFeature(HID_PD_MANUFACTUREDATE, &iManufacturerDate, sizeof(iManufacturerDate));
/*
for (uint8_t i=0;i<32;i++) {
FeatureList[i].Index=i;
FeatureList[i].Size=1;
FeatureBuffer[i]=i;
}
for (uint8_t i=0;i<32;i++) {
Serial0_print(i);
Serial0_print_c(' ');
Serial0_print(FeatureList[i].Index);
Serial0_print_c(' ');
Serial0_print(FeatureList[i].Size);
Serial0_println_c(' ');
}
for (uint8_t i=0;i<FeatureRecord;i++) {
Serial0_print(FeatureBuffer[i]); Serial0_print_c(' ');
}
*/
USBInit();
bitSet(iPresentStatus, PRESENTSTATUS_CHARGING);
bitSet(iPresentStatus, PRESENTSTATUS_ACPRESENT);
bitSet(iPresentStatus , PRESENTSTATUS_BATTPRESENT);
bitSet(iPresentStatus , PRESENTSTATUS_PRIMARYBATTERY);
recvStr[0] = HID_PD_PRESENTSTATUS;
recvStr[1] = iPresentStatus & 0xFF;
recvStr[2] = (iPresentStatus >> 8) & 0xFF;;
USB_EP3_send(recvStr, 3);
setFeature(HID_PD_PRESENTSTATUS, &iPresentStatus, sizeof(iPresentStatus));
/*
iRemaining = 40;
recvStr[0] = HID_PD_REMAININGCAPACITY;
recvStr[1] = iRemaining & 0xFF;
USB_EP3_send(recvStr, 2);
setFeature(HID_PD_REMAININGCAPACITY, &iRemaining, sizeof(iRemaining));
*/
}
void loop() {
while (USBSerial_available()) {
uint8_t serialChar = USBSerial_read();
recvStr[recvStrPtr++] = serialChar;
if (recvStrPtr == 4) {
/*
for (uint8_t i = 0; i < 9; i++) {
Serial0_write(recvStr[i]);
}
*/
if (recvStr[0] == HID_PD_PRESENTSTATUS) {
//USB_EP3_send(recvStr, 3);
iPresentStatus=recvStr[1]+(recvStr[2]<<8);
Serial0_print("ps:");
Serial0_println(iPresentStatus);
}
if (recvStr[0] == HID_PD_REMAININGCAPACITY) {
iRemaining=recvStr[1];
Serial0_print("rm:");
Serial0_println(iRemaining);
}
if (recvStr[0] == OnBoardLED) {
set_pixel_for_GRB_LED(ledData, 0, recvStr[1], recvStr[2], recvStr[3]);
neopixel_show_P1_5(ledData, NUM_BYTES);
}
recvStrPtr = 0;
}
Elsp = millis();
}
// If there is no data in 100ms, clear the receive buffer
if (millis() - Elsp > 100) {
recvStrPtr = 0;
Elsp = millis();
}
if((iPresentStatus != iPreviousStatus) || (iRemaining!=iPrevRemaining) || (millis()-iIntTimer>MINUPDATEINTERVAL) ) {
recvStr[0]=HID_PD_REMAININGCAPACITY;
recvStr[1]=iRemaining;
USB_EP3_send(recvStr, 2);
setFeature(HID_PD_REMAININGCAPACITY, &iRemaining, sizeof(iRemaining));
recvStr[0]=HID_PD_PRESENTSTATUS;
recvStr[1]=iPresentStatus&0xFF;
recvStr[2]=(iPresentStatus>>8)&0xFF;
USB_EP3_send(recvStr, 3);
setFeature(HID_PD_PRESENTSTATUS, &iPresentStatus, sizeof(iPresentStatus));
Serial0_println("a:");
iPreviousStatus=iPresentStatus;
iPrevRemaining=iRemaining;
iIntTimer=millis();
}
}
简单的说,开始之后,通过 HID Descriptor 报告当前设备属性,其中有很多 Feature项目。之后 Arduino 代码通过下面这种将描述符中的 Report ID 和 数值关联起来。后面,当Ch554收到 Feature Request 之后就根据前面的注册信息返回对应值。
setFeature(HID_PD_DESIGNCAPACITY, &iDesignCapacity, sizeof(iDesignCapacity));
除了USB HID 设备,Ch554还实现了一个 USB CDC 设备,在 loop 中我们接收来自USB 串口的数据,如果是以HID_PD_PRESENTSTATUS 开头的,或者HID_PD_REMAININGCAPACITY开头的,那么直接更改状态,然后从HID 对应的 EndPoint中发送出去,这样 Windows 接收到后会更新电池状态。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Management;
namespace ConsoleApp4
{
class Program
{
static void Main(string[] args)
{
string targetVid = "VID_1209";
string targetPid = "PID_C55C";
string portName = FindUsbDevicePort(targetVid, targetPid);
if (!string.IsNullOrEmpty(portName))
{
Console.WriteLine($"Found device on port: {portName}");
}
else
{
Console.WriteLine("Device not found.");
}
Console.ReadKey();
}
static string FindUsbDevicePort(string vid, string pid)
{
string query = "SELECT * FROM Win32_PnPEntity WHERE DeviceID LIKE '%" + vid + "&" + pid + "%'";
using (ManagementObjectSearcher searcher = new ManagementObjectSearcher(query))
{
foreach (ManagementObject device in searcher.Get())
{
string deviceId = device["DeviceID"]?.ToString();
if (deviceId != null && deviceId.Contains(vid) && deviceId.Contains(pid))
{
string caption = device["Caption"]?.ToString();
if (caption != null && caption.Contains("(COM"))
{
int startIndex = caption.IndexOf("(COM") + 1;
int endIndex = caption.IndexOf(")", startIndex);
return caption.Substring(startIndex, endIndex - startIndex);
}
}
}
}
return null;
}
}
}
完整的 Arduino 代码:
完整的 C#代码: