C# 取得设备 Bus reported device description 的方法

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Management;
using System.Collections;

namespace ConsoleApplication29
{
    class Program
    {
        private static void GatherUsbInformation()
        {
            var mos = new ManagementObjectSearcher("select DeviceID from Win32_PnPEntity");
            var mbos = new ArrayList(mos.Get());
            var data = new Dictionary<string, string[]>();

            for (var i = 0; i < mbos.Count; i++)
            {
                var managementBaseObject = mbos[i] as ManagementBaseObject;

                if (managementBaseObject == null)
                {
                    continue;
                }

                var deviceId = managementBaseObject.Properties["DeviceID"].Value as string;

                if (deviceId == null || !deviceId.StartsWith("USB"))
                {
                    continue;
                }

                if (!data.ContainsKey(deviceId))
                {
                    data.Add(deviceId, new string[8]);
                }
                else if (data.ContainsKey(deviceId))
                {
                    continue;
                }

                var mo = managementBaseObject as ManagementObject;
                var inParams = mo.GetMethodParameters("GetDeviceProperties");

                var result = mo.InvokeMethod(
                    "GetDeviceProperties",
                    inParams,
                    new InvokeMethodOptions()
                );

                if (result?.Properties["deviceProperties"].Value == null)
                {
                    continue;
                }

                foreach (var deviceProperties in result.Properties["deviceProperties"].Value as ManagementBaseObject[])
                {
                    var keyName = deviceProperties.Properties["KeyName"].Value as string;
                    var value = deviceProperties.Properties["Data"].Value as string;

                    if (string.IsNullOrWhiteSpace(value) || string.IsNullOrWhiteSpace(keyName))
                    {
                        //MachineInformationGatherer.Logger.LogTrace(
                        //    $"KeyName {keyName} or Value {value} was null or whitespace for device ID {deviceId}");
                        continue;
                    }
                    Console.WriteLine(keyName);
                    Console.WriteLine(value);
                    switch (keyName)
                    {
                        case "DEVPKEY_Device_BusReportedDeviceDesc":
                            {
                                data[deviceId][0] = value;
                                break;
                            }
                        case "DEVPKEY_Device_DriverDesc":
                            {
                                data[deviceId][1] = value;
                                break;
                            }
                        case "DEVPKEY_Device_DriverVersion":
                            {
                                data[deviceId][2] = value;
                                break;
                            }
                        case "DEVPKEY_Device_DriverDate":
                            {
                                var year = int.Parse(value.Substring(0, 4));
                                var month = int.Parse(value.Substring(4, 2));
                                var day = int.Parse(value.Substring(6, 2));
                                var hour = int.Parse(value.Substring(8, 2));
                                var minute = int.Parse(value.Substring(10, 2));
                                var second = int.Parse(value.Substring(12, 2));

                                data[deviceId][3] =
                                    new DateTime(year, month, day, hour, minute, second).ToString();
                                break;
                            }
                        case "DEVPKEY_Device_Class":
                            {
                                data[deviceId][4] = value;
                                break;
                            }
                        case "DEVPKEY_Device_DriverProvider":
                            {
                                data[deviceId][5] = value;
                                break;
                            }
                        case "DEVPKEY_NAME":
                            {
                                data[deviceId][6] = value;
                                break;
                            }
                        case "DEVPKEY_Device_Manufacturer":
                            {
                                data[deviceId][7] = value;
                                break;
                            }
                        case "DEVPKEY_Device_Children":
                            {
                                var children = deviceProperties.Properties["DEVPKEY_Device_Children"];
                                if (children.Value != null)
                                {
                                    if (children.IsArray)
                                    {
                                        foreach (var child in children.Value as string[])
                                        {
                                            mos.Query = new ObjectQuery(
                                                $"select * from Win32_PnPEntity where DeviceID = {child}");
                                            var childs = mos.Get();

                                            foreach (var child1 in childs)
                                            {
                                                mbos.Add(child1);
                                            }
                                        }
                                    }
                                }

                                break;
                            }
                    }
                }
            }
        }

        static void Main(string[] args)
        {
            GatherUsbInformation();
            Console.ReadKey();
        }
    }
}

运行结果:

运行结果

代码来自 https://github.com/L3tum/HardwareInformation/blob/master/HardwareInformation/Providers/WindowsInformationProvider.cs

Ch340B修改字符串等设备信息的实验

CH340B 是 CH34X USB 转串口芯片的一员,这个家族芯片之间的差别主要在于:

1.是否需要外部晶振(不需要的可以节省PCB空间);

2.封装尺寸差别;

CH340 封装

3.支持速度有差别,比如, CH340R 最高只支持 115200

CH340B 最大的特点在于内置了 EEPROM 可以修改默认的 USB 设备参数。

运行界面如下(我的操作系统是英文,所以一些位置出现乱码)。

CH340B 设定工具

可以看到,能够修改的有3个参数:

  1. PID/VID
  2. Product String
  3. Serial Numbers

接下来逐个介绍上面的参数。首先是 PID/VID。这个参数是主机用来识别 USB的最重要参数。比如,我将VID 修改为 0x8888,那么之前安装好的 CH340 驱动将无法使用(因为驱动的 INF 中找不到 VID=0x8888 PID=0x7523对应的项目):

修改 CH340 的 PID 和 VID

接下来修改 CH340 的驱动文件,手工添加新的项目:

这样修改之后, 驱动中签名会出现问题,如果想安装必须先 Disable Secure Boot 功能,安装时会出现下面的提示信息:

提示当前驱动签名有问题

安装之后再打开 Secure Boot,设备仍然能正常工作。但是如果始终打开 Secure Boot,那就一直无法安装。

之后再 Enable SecureBoot 设备驱动还是能够正常工作的

接下来介绍一下 Product String,这个修改之后,没有安装之前这个字符串会显示在设备上:

未安装驱动

安装之后会显示为驱动定义的名称:

安装驱动之后

同样,这个信息会显示在设备的“Bus reported device description”中:

“Bus reported device description”

最后,说一下Serial Numbers,修改这个项目之后,例如,修改这个项目为 20210705 之后:

修改 Serial Numbers 信息

在 Device instance path 中PID/VID 字符串的后面可以看到:

可以看到我们新加的 Serial Number出现在“Device instance path”属性中

本文提到的 CH340B 修改工具可以在这里下载:

UEFI Shell Helper

在编写调试 UEFI Shell 下的 Application 的时候,经常需要在实体机上进行测试。但是每次需要使用U盘来回拷贝,感觉非常麻烦。为了解决这个问题,最近几个月,制作了一个调试助手: UEFI Shell Helper 缩写 USH。

UEFI Shell Helper 缩写 USH

上面有2个 USB 接口,一个是 CH340 提供的 USB 串口(USB Type-A 公头),另外一个是 ESP32-S2 模拟出来的 U盘(USB Type-B 母头)。这个模拟 U盘中还内置了一个 UEFI Shell ,可以直接启动到 Shell 下。使用时,将母头端接入被测机,然后公头端接入Windows 主机端。之后可以通过主机对设备发送数据,发送完成后运行 Shell 下的应用程序即可取得数据;反之被测试端也可以发送数据给主机端。这样在调试的时候就可以免除插拔拷贝之苦。

工作的视频:

https://www.bilibili.com/video/BV1T64y1e7ay/

这是一个开源设备,具体设计方案可以在下面的链接看到(涉及电路图,PCB 设计, C# 编程和 Arduino 代码开发,前后搞了3个月,感觉挺复杂):

https://diy.szlcsc.com/p/Zoologist/uefi-shell#

有兴趣的朋友可以看看,不过目前还有1个问题尚未解决:

1.串口传输最高只能达到 230400 baud rate (115200*2),选用的 CH340B 应该可以达到 2000000;目前正在找 WCH Debug。

后面会介绍 USH 牵涉到的 UEFI 内容.

ESP32 I2C 设备扫描代码

有时候在调试 I2C 设备的时候,需要确定设备的地址,可以使用下面的代码来完成:

// ESP32 I2C Scanner
// Based on code of Nick Gammon  http://www.gammon.com.au/forum/?id=10896
// ESP32 DevKit - Arduino IDE 1.8.5
// Device tested PCF8574 - Use pullup resistors 3K3 ohms !
// PCF8574 Default Freq 100 KHz 

#include <Wire.h>

void setup()
{
  Serial.begin (115200);  
  Wire.begin (21, 22);   // sda= GPIO_21 /scl= GPIO_22
}

void Scanner ()
{
  Serial.println ();
  Serial.println ("I2C scanner. Scanning ...");
  byte count = 0;

  Wire.begin();
  for (byte i = 8; i < 120; i++)
  {
    Wire.beginTransmission (i);          // Begin I2C transmission Address (i)
    if (Wire.endTransmission () == 0)  // Receive 0 = success (ACK response) 
    {
      Serial.print ("Found address: ");
      Serial.print (i, DEC);
      Serial.print (" (0x");
      Serial.print (i, HEX);     // PCF8574 7 bit address
      Serial.println (")");
      count++;
    }
  }
  Serial.print ("Found ");      
  Serial.print (count, DEC);        // numbers of devices
  Serial.println (" device(s).");
}

void loop()
{
  Scanner ();
  delay (100);
}

代码来自 https://www.esp32.com/viewtopic.php?p=55303

ESP32 和 C# 的 CRC16

CRC-16 有很多种,这里使用的是CRC-16/CCITT-FALSE。可以用这个在线工具进行验证(注意下图是16进制数值):

https://crccalc.com/

1.

static unsigned short crc16(const unsigned char *buf, unsigned long count)
{
  unsigned short crc = 0xFFFF;
  int i;

  while(count--) {
    crc = crc ^ *buf++ << 8;

    for (i=0; i<8; i++) {
      if (crc & 0x8000) crc = crc << 1 ^ 0x1021;
      else crc = crc << 1;
    }
  }
  return crc;
}

void setup() {
        Serial.begin(115200);
}

void loop() {
        byte data[11]={0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88,0x99,0x00,0x11};
        Serial.println(crc16(data,11),HEX);
        delay(10000);
}

2.C# 代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices;

namespace CRC16Test
{
    class Program
    {


        static void Main(string[] args)
        {
            byte[] data = new byte[] { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00,0x11};
            /*           for (int i = 0; i < 10; i++)
                       {
                           data[i] = (byte)(i % 256);
                       }
           */
            string hex = Crc16Ccitt(data).ToString("x2");
            Console.WriteLine(hex);

            Console.Write("Press any key to continue . . . ");
            Console.ReadKey(true);
        }

        //
        // CRC-16/CCITT-FALSE
        // https://crccalc.com/
        //
        public static ushort Crc16Ccitt(byte[] bytes)
        {
            const ushort poly = 0x1021;
            ushort[] table = new ushort[256];
            ushort initialValue = 0xffff;
            ushort temp, a;
            ushort crc = initialValue;
            for (int i = 0; i < table.Length; ++i)
            {
                temp = 0;
                a = (ushort)(i << 8);
                for (int j = 0; j < 8; ++j)
                {
                    if (((temp ^ a) & 0x8000) != 0)
                        temp = (ushort)((temp << 1) ^ poly);
                    else
                        temp <<= 1;
                    a <<= 1;
                }
                table[i] = temp;
            }
            for (int i = 0; i < bytes.Length; ++i)
            {
                crc = (ushort)((crc << 8) ^ table[((crc >> 8) ^ (0xff & bytes[i]))]);
            }
            return crc;
        }

    }
}

中文版和英文版的超级终端

从 Windows XP 的中文版和英文版中拷贝而来的超级终端程序。最主要是它提供了 Xmodem,YModem和ZModem 文件传输协议,可以用来进行测试。

Windows XP 的超级终端

=====================================================

2023年1月13日更新

上述的超级终端在 Win10 下可以正常使用,但是运行之后不会出现图标。经过研究这个问题可以通过添加hticons.dll来解决。有需要的朋友可以在下面下载这个文件,放到超级终端同一个目录下即可。

FireBeetle “软”USB Host Shield

喜欢军事的朋友一定经常听过这句话,这就是“只要推力大,板砖也能飞上天”。实际上,这句话并非玩笑之语,确有出处。当年,F-22是从“先进战术战斗机”(简称ATF)计划发展而来的。早期,好几家公司一起竞标,分别是波音,通用动力,洛克希德,诺思罗普,和麦道公司。大家提概念设计方案。结果,可能是因为洛克希德曾经设计出F-117隐形飞机的缘故,走老路当然是最轻松的,所以,洛克希德最初提出的F-22前身方案,它就像是F-117的放大版。设计跟F-117这个怪胎一样,为了隐身性能,以牺牲空气动力学为代价,飞机设计总重36吨。后来,有个人是这么评价这种早期设计的,他就是洛克希德在ATF计划中的项目经理巴特·奥斯本(bart osborne),1998年,一家杂志对奥斯本进行采访【参考1】,而他是这样说的:“我们知道,这个设计将会存在严重的超音速问题,我们的设计当然可以超音速飞行,但肯定蠢得像猪。显然,只要推力大,板砖也能飞上天(With enough power, you can make a brick fly)”

这句话出处就是这么来的,他当时想表达的本意是这样的:我们设计的这种飞机, 在高空高速上肯定会存在很多问题,当然,这并不是说,这种设计不能超音速飞行,完全不是,因为只要推力,你也能让砖头飞上天,也能它超音速。

巴特·奥斯本的评价很中肯,洛克希德提交这个概念设计方案后,美国空军对这种设计的评价最差,在其他7个项目设计中,排倒数第一。【参考1】

对于我们来说,只要 CPU 足够快,GPIO 足够快,能够模拟出各种总线,最常见的是软串口,再稍微复杂一些的是I2C, SPI。最近有一个项目使用 ESP32 来模拟 USB Host,当前因为 GPIO 不够快,目前只能解析键盘鼠标这样的低速设备(Low Speed)。项目地址是:https://github.com/sdima1357/esp32_usb_soft_host 当然,对于Arduino 玩家来说可以直接使用下面这个库 :https://github.com/tobozo/ESP32-USB-Soft-Host

库可以同时支持4个 USB Host, 这里我设计一个 FireBeetle Shield 提供了 2个 USB Host。具体电路如下:

PCB 如下:

Soft USB Host 和 FireBeetle

可以看到板子上有2个 USB 母头和1个USB 公头,其中母头是给 USB Host使用的,公头是为了在供电不足情况下通过充电宝之类直接对设备供电。库可以支持4组USB Host ,经过实验在FireBeetle上可以使用下面的组合:

16131925
17151826

测试代码:

#include <ESP32-USBSoftHost.hpp>
#include "usbkbd.h" // KeyboardReportParser

  // default pins tested on FireBeetle
  // +  16 13 19 25
  // -  17 15 18 26
  #define DP_P0  25  // always enabled
  #define DM_P0  26  // always enabled
  #define DP_P1  19 // -1 to disable
  #define DM_P1  18 // -1 to disable
  #define DP_P2  -1 // -1 to disable
  #define DM_P2  -1 // -1 to disable
  #define DP_P3  -1 // -1 to disable
  #define DM_P3  -1 // -1 to disable



static void my_USB_DetectCB( uint8_t usbNum, void * dev )
{
  sDevDesc *device = (sDevDesc*)dev;
  printf("New device detected on USB#%d\n", usbNum);
  printf("desc.bcdUSB             = 0x%04x\n", device->bcdUSB);
  printf("desc.bDeviceClass       = 0x%02x\n", device->bDeviceClass);
  printf("desc.bDeviceSubClass    = 0x%02x\n", device->bDeviceSubClass);
  printf("desc.bDeviceProtocol    = 0x%02x\n", device->bDeviceProtocol);
  printf("desc.bMaxPacketSize0    = 0x%02x\n", device->bMaxPacketSize0);
  printf("desc.idVendor           = 0x%04x\n", device->idVendor);
  printf("desc.idProduct          = 0x%04x\n", device->idProduct);
  printf("desc.bcdDevice          = 0x%04x\n", device->bcdDevice);
  printf("desc.iManufacturer      = 0x%02x\n", device->iManufacturer);
  printf("desc.iProduct           = 0x%02x\n", device->iProduct);
  printf("desc.iSerialNumber      = 0x%02x\n", device->iSerialNumber);
  printf("desc.bNumConfigurations = 0x%02x\n", device->bNumConfigurations);
  // if( device->iProduct == mySupportedIdProduct && device->iManufacturer == mySupportedManufacturer ) {
  //   myListenUSBPort = usbNum;
  // }
}


static void my_USB_PrintCB(uint8_t usbNum, uint8_t byte_depth, uint8_t* data, uint8_t data_len)
{
  // if( myListenUSBPort != usbNum ) return;
  printf("in: ");
  for(int k=0;k<data_len;k++) {
    printf("0x%02x ", data[k] );
  }
  printf("\n");
}

usb_pins_config_t USB_Pins_Config =
{
  DP_P0, DM_P0,
  DP_P1, DM_P1,
  DP_P2, DM_P2,
  DP_P3, DM_P3
};


void setup()
{

  Serial.begin(115200);
  delay(200);
  Serial.println("USB Test");
  delay(1000);

  USH.init( USB_Pins_Config, my_USB_DetectCB, my_USB_PrintCB );

}

void loop()
{
  vTaskDelete(NULL);
}


运行结果, 上面是输出的Descriptor,下面是收到的鼠标数据:

目前这个库还存在一些兼容性问题,并非所有的鼠标都能正常工作。我测试了微软的IO1.01 是可以正常工作的。

参考:

  1. http://www.codeonemagazine.com/f22_article.html?item_id=179 Design Evolution Of The F-22 Raptor
  2. http://www.nanshannews.cn/cj/cj9059.html

CH55x 串口速度测试

使用上次的工具来测试 Ch55x(Ch551/2/4)开发板的串口速度。

实际测试的是 CH554 ,使用 Ch55xduino 编写代码如下:

#ifndef USER_USB_RAM
#error "This example needs to be compiled with a USER USB setting"
#endif

#include "src/userUsbCdc/USBCDC.h"

//This is a fairly large array, store it in external memory with keyword __xdata
__xdata char recvStr[64];

// 代码中出现的发送和接收
// 测试 Package 的大小
#define MAX_N 256

// 2秒超时
#define TIMEOUT  2000

// 接收数据 Buffer
// 设置为实际的2倍大小避免接收超过Buffer 的问题
__xdata byte receiveBuffer[MAX_N];

void setup() {
  USBInit();
}

// 状态机记录
byte currentStatus = 0;

// 收到的数量
int counter = 0;
// 超时计时器
unsigned long Elsp;
// 发送标记,True 时发送,反之不发送
boolean sendOnce = true;
// 发生错误的数量
int failCount;

void loop() {
  switch (currentStatus) {
    case 0:
      // 初始状态
      while (USBSerial_available()) {
        byte c = USBSerial_read();
        // q命令查询当前状态(用于调试)
        if (c == 'q') {
          USBSerial_print_s("Current 0");
          USBSerial_flush();
        }
        // 1(接收模式) 2(发送模式)
        if ((c == '1') || (c == '2')) {
          // 转移到下一个状态
          currentStatus = c - '0';
          // 如果是接收模式,那么开始计时
          if (c == '1') {
            Elsp = millis();
            counter = 0;
          }
        }
      }
      break;

    case 1: //接收模式
      // 如果接收模式超时,那么直接返回到状态0
      if (millis() - Elsp > TIMEOUT) {
        // 返回状态 0
        currentStatus = 0;
        // 发送 "aaa" 表示当前超时
        USBSerial_write('a'); USBSerial_write('a'); USBSerial_write('a');
        USBSerial_flush();
      }

      // 接收数据
      while (USBSerial_available()) {
        byte c = USBSerial_read();
        receiveBuffer[counter] = c;
        // 统计接收到的数据量
        counter++;

        // 如果接收到足够的数据,那么转移到状态3 进行校验
        if (counter == MAX_N) {
          currentStatus = 3;
          //Serial.print("St3");
        }
      }
      break;
    case 3:
      // 校验接收模式收到的数据,数据是 0x55 0xaa 0x55....
      failCount = 0;
      for (int i = 0; i < MAX_N; i = i + 2) {
        if (receiveBuffer[i] != 0x55) {
          failCount++;
        }
        if (receiveBuffer[i + 1] != 0xAA) {
          failCount++;
        }
      }
      // 检查是否有错误
      if (failCount == 0) {
        // 返回"ppp" 表示校验通过
        USBSerial_write('p'); USBSerial_write('p'); USBSerial_write('p');
        USBSerial_flush();
      }
      else {
        // 校验失败,返回 'f'+发生错误的数量错误数据
        USBSerial_write('f');
        USBSerial_write((byte)(failCount & 0xFF));
        USBSerial_write((byte)(failCount >> 8));

      }
      counter = 0;
      //Serial.println("Switch to status 1");
      Elsp = millis();
      currentStatus = 1;
      break;
    case 2:
      // 发送模式,直接发送 MAX_N 个 0x55 0xaa 0x55 ....
      if (sendOnce) {
        for (int i = 0; i < MAX_N / 2; i++) {
          USBSerial_write(0x55);
          USBSerial_write(0xAA);
          USBSerial_flush();
        }
        sendOnce = false;
      }
      while (USBSerial_available()) {
        char c = USBSerial_read();
        if (c == '0') {
          currentStatus = 0;
          //Serial.print("Switch to 0");
        }
        if (c == '2') {
          sendOnce = true;
        }
        // 查询状态
        if (c == 'q') {
          USBSerial_print_s("Current 2");
          USBSerial_flush();
        }
      }
      break;
    default: {
        USBSerial_println_s("Unknown status");
        USBSerial_flush();
      }
  }

1.PC端接收,Ch554发送,可以看到速度能达到37K/S左右

2.PC端发送,Ch554接收,可以看到速度能达到97K/S左右

从结果可以看出来,比 32U4 这样的芯片还是快一点点的(测试模式是 5V 24Mhz)

Win10 下通过BCDEDIT 增加启动选项

最近在看《软件调试(第二版)》,第四章讲述启动过程的时候作者提到了 bcdedit 相关内容。这次根据指引进行实验。

1.创建一个新的引导项目,这里我们只是将目前的引导选项多制作一个拷贝

通过拷贝生成一个引导选项

2.重启后会出现下面的界面,可以选择

选择引导项

3.《软件调试》书中介绍了下面的命令可以将这个界面切换为 Win7 类似的

切换为 Windows7 风格的启动菜单

4.之后重启启动界面变成下面这种

5.可以在系统下运行 msconfig 编辑启动项

MSConfig

6.使用下面的命令可以恢复为 2. 这种样式

恢复为 Windows 10 的方法

参考

  1. https://www.howtogeek.com/245175/how-to-add-safe-mode-to-the-windows-8-and-10-boot-menu/ How to Add Safe Mode to the Windows 8 and 10 Boot Menu
  2. https://blog.csdn.net/qq_25368751/article/details/80660286 多系统的福音,bcdedit找回传统启动菜单。取消(恢复)Modern UI风格启动菜单~菜单~

SerialRead() 结果为何不是想要的?

代码如下,功能很简单:判断输入的是否为 0xaa, 如果是就输出一段字符,如果不是输出另外一段:

void setup() {
  Serial.begin(9600);
}

void loop() {
  while (Serial.available()) {
    char c = Serial.read();
    if (c == 0xaa) {
      Serial.println("Get 0xaa");
    } else {
      Serial.print("Not 0xaa. You have input ");
      Serial.println(c,HEX);
    }
  }
}

实验平台是 Leonardo, 实验结果如下:

运行结果

为什么输入的 0xaa 会被判定为 ffffffaa

在  \arduino-1.8.4\hardware\arduino\avr\cores\arduino\CDC.cpp 有如下定义:

int Serial_::read(void)
{
	if (peek_buffer >= 0) {
		int c = peek_buffer;
		peek_buffer = -1;
		return c;
	}
	return USB_Recv(CDC_RX);
}

对于 0xaa 来说,转换为 int 之后的结果就是 ffffffaa,可以用下面的代码来验证:

char c=0xaa;
Serial.println((int)c,HEX);

结果

知道了原因,修改方法也很简单,在变量声明的地方定义为 byte c 即可。