
PVD: 虚拟电池
简介
基于 CH554 实现一个 Windows 下的虚拟电池(USB HID UPS),这样可以方便用户进行性能/功耗方面的测试。
简介:基于 CH554 实现一个 Windows 下的虚拟电池(USB HID UPS),这样可以方便用户进行性能/功耗方面的测试。开源协议
:GPL 3.0
描述
前面的 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
#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 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 接收到后会更新电池状态。
为了更简单的控制这一套,还使用 C#编写了一个上位机代码,会自动查找设备所在的串口,设置选择 AC/DC 之后,点击设置就可以将数据发给设备,设备再回馈给 Windows实现电池的设定。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Management;
using System.IO.Ports;
using System.Threading;
namespace VBatCon
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
static string portName;
static UInt16 iPresentStatus= 0x0D; //Charing,AC,BattPresent
private void groupBox1_Enter(object sender, EventArgs e)
{
}
private void trackBar1_Scroll(object sender, EventArgs e)
{
label2.Text = trackBar1.Value.ToString() + "%";
}
private void label3_MouseClick(object sender, MouseEventArgs e)
{
System.Diagnostics.Process.Start("http://www.lab-z.com/pvdb");
}
private void button1_Click(object sender, EventArgs e)
{
if (button1.Text == "AC")
{
button1.Text = "DC";
iPresentStatus = 0x0A;
}
else
{
button1.Text = "AC";
iPresentStatus = 0x0D;
}
}
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;
}
private void Form1_Load(object sender, EventArgs e)
{
string targetVid = "VID_120B";
string targetPid = "PID_2026";
portName = FindUsbDevicePort(targetVid, targetPid);
if (portName == null)
{
label1.Text = "Not Found!";
button2.Enabled = false;
}
else
{
label1.Text = "Found PVD at " + portName;
}
}
private void button2_Click(object sender, EventArgs e)
{
Byte[] Buffer = new Byte[4];
serialPort1.PortName = portName;
serialPort1.BaudRate = 115200;
serialPort1.DataBits = 8;
serialPort1.Parity = 0;
serialPort1.StopBits = (StopBits)1;
serialPort1.Encoding = System.Text.Encoding.GetEncoding(28591);
serialPort1.DtrEnable = true;
serialPort1.RtsEnable = true;
serialPort1.Open();
// 设置红色 LED 亮起
Array.Clear(Buffer, 0, Buffer.Length);
Buffer[0] = 0x03; Buffer[1] = 0x10;
serialPort1.Write(Buffer, 0, Buffer.Length);
Array.Clear(Buffer, 0, Buffer.Length);
Buffer[0] = 0x07; //HID_PD_PRESENTSTATUS
Buffer[1] = Convert.ToByte(iPresentStatus & 0xFF);
Buffer[2] = Convert.ToByte((iPresentStatus >>8) & 0xFF);
serialPort1.Write(Buffer, 0, Buffer.Length);
Array.Clear(Buffer, 0, Buffer.Length);
Buffer[0] = 0x0C; //HID_PD_REMAININGCAPACITY
Buffer[1] = Convert.ToByte(trackBar1.Value*255 / 100);
serialPort1.Write(Buffer, 0, Buffer.Length);
//Thread.Sleep(1000);
// 熄灭 LED
Array.Clear(Buffer, 0, Buffer.Length);
Buffer[0] = 0x03; Buffer[1] = 0x00;
serialPort1.Write(Buffer, 0, Buffer.Length);
serialPort1.Close();
}
}
}
工作的视频可以在 B站看到
https://www.bilibili.com/video/BV13xcTeAEMG/
设计图

BOM


评论