You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Raspberry Pi Pico+TinyUSB的自定义UPS无法被UPower识别充电状态

Raspberry Pi Pico + TinyUSB 自定义UPS无法被Ubuntu UPower识别充电状态

我用Raspberry Pi Pico和TinyUSB开发自定义UPS,希望能被Ubuntu主机的UPower识别。目前设备能被检测到,但UPower显示电池为空,Plasma桌面电池指示器也不显示充电状态,仿佛没收到设备的报告。我想让UPower正确响应设备发送的充电状态报告,期望state字段显示为charging。

当前UPower输出

root@user-HP:~# upower -i /org/freedesktop/UPower/devices/ups_hiddev0
  native-path:          /sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.0/usbmisc/hiddev0
  vendor:               APC
  model:                UPS
  serial:               123456
  power supply:         yes
  updated:              Sun, 02 Apr 2023 09:35:04 +0300 (16 seconds ago)
  has history:          yes
  has statistics:       yes
  ups
    present:             yes
    state:               empty
    warning-level:       none
    percentage:          0%
    icon-name:          'battery-empty-symbolic'

设备报告描述符

原始字节

05 84 09 04 a1 01 09 10 a1 00 85 01 75 08 95 01 09 fd 79 01 b1 03 09 fe 79 02 b1 03 09 ff 79 03 b1 03 09 12 a1 00 85 02 05 85 75 01 95 04 17 00 00 00 00 27 01 00 00 00 09 44 09 45 09 46 09 47 81 82 95 01 75 04 81 01 c0 c0 c0

解析后的结构

INPUT(2)[INPUT]
    Field(0)
      Physical(Power Device.Battery)
      Application(Power Device.UPS)
      Usage(4)
        Battery System.Charging
        Battery System.Discharging
        Battery System.0046
        Battery System.0047
      Logical Minimum(0)
      Logical Maximum(1)
      Report Size(1)
      Report Count(4)
      Report Offset(0)
      Flags( Variable Absolute Volatile )
  FEATURE(1)[FEATURE]
    Field(0)
      Physical(Power Device.BatterySystem)
      Application(Power Device.UPS)
      Usage(1)
        Power Device.iManufacturer
      Report Size(8)
      Report Count(1)
      Report Offset(0)
      Flags( Constant Variable Absolute )
    Field(1)
      Physical(Power Device.BatterySystem)
      Application(Power Device.UPS)
      Usage(1)
        Power Device.iProduct
      Report Size(8)
      Report Count(1)
      Report Offset(8)
      Flags( Constant Variable Absolute )
    Field(2)
      Physical(Power Device.BatterySystem)
      Application(Power Device.UPS)
      Usage(1)
        Power Device.iSerialNumber
      Report Size(8)
      Report Count(1)
      Report Offset(16)
      Flags( Constant Variable Absolute )

Battery System.Charging ---> Sync.Report
Battery System.Discharging ---> Sync.Report
Battery System.0046 ---> Sync.Report
Battery System.0047 ---> Sync.Report

人类可读的HID描述符

USAGE_PAGE (Power Device)
USAGE (UPS)
COLLECTION (Application)
  USAGE (Battery System)
  COLLECTION (Physical)
    REPORT_ID (1)
    REPORT_SIZE (8)
    REPORT_COUNT (1)

    USAGE (iManufacturer)
    STRING_INDEX (1)
    FEATURE (Constant Variable Absolute)

    USAGE (iProduct)
    STRING_INDEX (2)
    FEATURE (Constant Variable Absolute)

    USAGE (iSerialNumber)
    STRING_INDEX (3)
    FEATURE (Constant Variable Absolute)

    USAGE (Battery)
    COLLECTION (Physical)
      REPORT_ID (2)
      USAGE_PAGE (Battery System)
      REPORT_SIZE (1)
      REPORT_COUNT (4)
      LOGICAL_MINIMUM (0)
      LOGICAL_MAXIMUM (1)
      USAGE (Charging)
      USAGE (Discharging)
      USAGE (Fully Charged)
      USAGE (Fully Discharged)
      INPUT (Variable Absolute Volatile)

      REPORT_COUNT (1)
      REPORT_SIZE (4)
      INPUT (Constant Array)
    END_COLLECTION
  END_COLLECTION
END_COLLECTION

Linux无法识别fully charged和fully discharged标志,我尝试移除这两个标志,仅保留charging,同时添加了absolute state of charge。

设备端main.cpp代码

#include "pico/stdlib.h"
#include "hardware/pwm.h"
#include "bsp/board.h"
#include "tusb.h"

int main() {
  board_init();
  tusb_init();

  uint32_t last_run = 0;
  uint8_t report1[] = {1, 2, 3};
  uint8_t report2[] = {0b00000001};
  while (1) {
    tud_task();

    // 每50ms发送一次数据
    if (board_millis() - last_run > 50 && tud_hid_ready() && !tud_suspended()) {
      last_run = board_millis();
      // tud_hid_report(1, report1, sizeof(report1)); // 同时发送两个报告时,只有#1能被主机接收
      tud_hid_report(2, report2, sizeof(report2));
    }
  }
}

// 收到GET_REPORT控制请求时调用
// 应用需填充报告内容并返回长度,返回0会导致栈暂停请求
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen) {
  switch (report_id) {
    case 1:
      buffer[0] = 1;
      buffer[1] = 2;
      buffer[2] = 3;
      return 3;
    case 2:
      buffer[0] = 0b00000001; // 报告充电状态
      return 1;
    default:
      return 0;
  }
}

// 收到SET_REPORT控制请求或OUT端点数据时调用(Report ID = 0, Type = 0)
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize) {
  // 回显收到的所有数据
  tud_hid_report(0, buffer, bufsize);
}

疑问:发送报告的正确方式是使用tud_hid_get_report_cb还是循环调用tud_hid_report?

主机端usbhid-dump输出

root@user-HP:~# usbhid-dump -a 1:119 -e all
001:119:000:DESCRIPTOR         1680420804.631776
 05 84 09 10 A1 01 85 01 75 08 95 01 09 FD 79 01
 B1 03 09 FE 79 02 B1 03 09 FF 79 03 B1 03 09 12
 A1 00 85 02 05 85 75 01 95 04 17 00 00 00 00 27
 01 00 00 00 09 44 09 45 09 46 09 47 81 82 95 01
 75 04 81 01 C0 C0

Starting dumping interrupt transfer stream
with 1 minute timeout.

001:119:000:STREAM             1680420804.643196
 02 01

001:119:000:STREAM             1680420804.694348
 02 01

001:119:000:STREAM             1680420804.745391
 02 01

001:119:000:STREAM             1680420804.796426
 02 01

001:119:000:STREAM             1680420804.847433
 02 01

001:119:000:STREAM             1680420804.898256
 02 01

配置描述符修改情况

我把TUD_HID_DESCRIPTOR改成TUD_HID_INOUT_DESCRIPTOR,端点号设为1。现在设备到主机的传输成功,不再报“没有该文件或目录”错误,但主机用0x81 IN端点发送报告,而非0x01 OUT端点。

usb_descriptors.cpp代码

#include "tusb.h"
#include "report-descriptor/descriptor.hpp"

//--------------------------------------------------------------------+
// Device Descriptors
//--------------------------------------------------------------------+
tusb_desc_device_t const desc_device = {
  .bLength = sizeof(tusb_desc_device_t),
  .bDescriptorType = TUSB_DESC_DEVICE,
  .bcdUSB = 0x0200,
  .bDeviceClass = 0x00,
  .bDeviceSubClass = 0x00,
  .bDeviceProtocol = 0x00,
  .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,

  // 使用APC的厂商ID,因为未知厂商的UPS不会被识别
  .idVendor = 0x051D,
  .idProduct = 0x0003,
  .bcdDevice = 0x0100,

  .iManufacturer = 0x01,
  .iProduct = 0x02,
  .iSerialNumber = 0x03,

  .bNumConfigurations = 0x01
};

// 收到GET DEVICE DESCRIPTOR请求时调用
// 应用返回描述符指针
uint8_t const *tud_descriptor_device_cb(void) {
  return (uint8_t const *)&desc_device;
}

//--------------------------------------------------------------------+
// HID Report Descriptor
//--------------------------------------------------------------------+

// 收到GET HID REPORT DESCRIPTOR请求时调用
// 应用返回描述符指针,描述符内容需在传输完成前保持存在
uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance) {
  return hid_report_descriptor_test.data();
}

//--------------------------------------------------------------------+
// Configuration Descriptor
//--------------------------------------------------------------------+

enum {ITF_NUM_HID, ITF_NUM_TOTAL};

#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_INOUT_DESC_LEN)
#define EPNUM_HID 0x01

uint8_t const desc_configuration[] = {
  // 配置编号、接口数量、字符串索引、总长度、属性、功耗(mA)
  TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),

  // 接口编号、字符串索引、协议、报告描述符长度、EP输入/输出地址、大小和轮询间隔
  TUD_HID_INOUT_DESCRIPTOR(
    ITF_NUM_HID,
    0,
    HID_ITF_PROTOCOL_NONE,
    hid_report_descriptor_test.size(),
    EPNUM_HID,
    0x80 | EPNUM_HID,
    CFG_TUD_HID_BUFSIZE,
    1
  )
};

// 收到GET CONFIGURATION DESCRIPTOR请求时调用
// 应用返回描述符指针,描述符内容需在传输完成前保持存在
uint8_t const *tud_descriptor_configuration_cb(uint8_t index)
{
  (void)index; // 多配置时使用
  return desc_configuration;
}
//--------------------------------------------------------------------+
// String Descriptors
//--------------------------------------------------------------------+

// 字符串描述符指针数组
char const *string_desc_arr[] = {
  (const char[]){0x09, 0x04}, // 0: 支持的语言为英语(0x0409)
  "American Power Conversion", // 1: 厂商
  "UPS", // 2: 产品
  "123456", // 3: 序列号,应使用芯片ID
  "LiFePO4", // 4: 电池化学类型 - 暂未使用
};

static uint16_t _desc_str[32];

// 收到GET STRING DESCRIPTOR请求时调用
// 应用返回描述符指针,描述符内容需在传输完成前保持存在
uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid)
{
  (void)langid;

  uint8_t chr_count;

  if (index == 0)
  {
    memcpy(&_desc_str[1], string_desc_arr[0], 2);
    chr_count = 1;
  }
  else
  {
    // 将ASCII字符串转换为UTF-16

    if (!(index < sizeof(string_desc_arr) / sizeof(string_desc_arr[0])))
      return NULL;

    const char *str = string_desc_arr[index];

    // 限制最大字符数
    chr_count = strlen(str);
    if (chr_count > 31)
      chr_count = 31;

    for (uint8_t i = 0; i < chr_count; i++)
    {
      _desc_str[1 + i] = str[i];
    }
  }

  // 第一个字节是长度(包含头部),第二个字节是字符串类型
  _desc_str[0] = (TUSB_DESC_STRING << 8) | (2 * chr_count + 2);

  return _desc_str;
}

内容的提问来源于stack exchange,提问作者Anton

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 20:11:59