PVD:Battery 虚拟电池

前面的 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#代码: