基于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
相关产品推荐
相关产品推荐

