嘉立创产业服务站群
站内搜索
发作品签到
专业版

PVD: 虚拟电池

工程标签

1.0k
0
0
12

简介

基于 CH554 实现一个 Windows 下的虚拟电池(USB HID UPS),这样可以方便用户进行性能/功耗方面的测试。

简介:基于 CH554 实现一个 Windows 下的虚拟电池(USB HID UPS),这样可以方便用户进行性能/功耗方面的测试。
星火计划2025
复刻成本:50

开源协议

GPL 3.0

创建时间:2025-01-06 06:42:41更新时间:2025-01-13 09:23:09

描述

前面的 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

暂无BOM

附件

序号文件名称下载次数
1
VBatCon.zip
20
2
vPowerDevice.zip
27
3
PVD虚拟电池功能测试.mp4
16
克隆工程
添加到专辑
0
0
分享
侵权投诉

评论

全部评论(1)
按时间排序|按热度排序
粉丝0|获赞0
相关工程
暂无相关工程

底部导航