乱序执行 (out-of-order execution)

乱序执行(out-of-order execution)是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理的技术。比方Core乱序执行引擎说程序某一段有7条指令,此时CPU将根据各单元电路的空闲状态和各指令能否提前执行的具体情况分析后,将能提前执行的指令立即发送给相应电路执行。

这好比请A、B、C三个名人为晚会题写横幅“春节联欢晚会”六个大字,每人各写两个字。如果这时在一张大纸上按顺序由A写好”春节”后再交给B写”联欢”,然后再由C写”晚会”,那么这样在A写的时候,B和C必须等待,而在B写的时候C仍然要等待而A已经没事了。但如果采用三个人分别用三张纸同时写的做法, 那么B和C都不必须等待就可以同时各写各的了,甚至C和B还可以比A先写好也没关系(就象乱序执行),但当他们都写完后就必须重新在横幅上(自然可以由别人做,就象CPU中乱序执行后的重新排列单元)按”春节联欢晚会”的顺序排好才能挂出去。【参考1】

从Intel 第六代 CPU(Pentuim Pro) 开始, 引入了乱序执行的功能。通过前面的介绍也可以看出来 CPU 需要确定后面要执行的指令并不依赖于前面的指令。比如:计算 a=1+2, b= a+1, 这种情况是无法先进行 b=a+1 运算的。这种依赖被称作依赖链。

首先,处理器将指令转为微指令(μop)。比如: ADD EAX,EBX 这样的只对应一个μop;但是像 ADD EAX,[MEM1] 会生成2个μop:一条指令是从内存读取数值到寄存器,另外一条是将保存内存值的寄存器和 EAX 相加;再例如 ADD [MEM1],EAX 会生成3个μop:第一个是从内存取值,一个是进行相加,最后一个是将结果传输到内存中。这样做的好处是,拆分之后的μop可以进行乱序执行:

例如:

mov eax,[mem1]
imul eax,5
add eax[mem2]
mov [mem3],eax

这里 ADD EAX,[MEM2] 拆分为2条μop。这样计算 imul eax, 5 的时候可以同时到内存中读取[MEM2]。如果Cache 中没有数据,那么处理器读取[MEM1]之后会马上进行读取[MEM2]的操作。

再比如堆栈操作指令拆分为μop之后运行会更有效率。例如:

push eax
call func

如果没有将 PUSH 拆分为2条μop的话,因为 CALL 操作需要依赖于 ESP ,所以 CALL 需要等待 PUSH EAX 之后才能执行。如果将 PUSH eax 指令拆分为2个μop: SUB ESP,4 和 MOV [ESP],EAX。 那么 SUB ESP,4 可以在 EAX 还没有赋值之前执行。这样就解除了CALL 指令的路径依赖。可见在μop的帮助下,堆栈的操作能够节省时间。【参考2】

参考:

1.https://baike.baidu.com/item/%E4%B9%B1%E5%BA%8F%E6%89%A7%E8%A1%8C/4944129?fr=aladdin

2.https://www.agner.org/optimize/microarchitecture.pdf

CISC 和 RISC

计算机指令就是指挥机器工作的指示和命令,程序就是一系列按一定顺序排列的指令,执行程序的过程就是计算机的工作过程。指令集,就是CPU中用来计算和控制计算机系统的一套指令的集合,而每一种新型的CPU在设计时就规定了一系列与其他硬件电路相配合的指令系统。比如:日常生活中,“去吃饭”这样的命令可以看作是简单的指令。根据我的观察,男人和女人在指令集上是存在差别的。男人理解的指令集通常只是女人指令集的子集。譬如说,女人对于“洗衣服”指令的理解是包括“将衣物放入洗衣机启动洗涤”,“洗衣完成后晾衣服”以及“晾干后收起来”;但是对于男人来说“洗衣服”指令只是 “将衣物放入洗衣机启动洗涤” 的含义。

指令集的先进与否,也关系到CPU的性能发挥,它也是CPU性能体现的一个重要标志。每款CPU在设计时就规定了一系列与其硬件电路相配合的指令系统。指令的强弱也是CPU的重要指标,指令集是提高微处理器效率的最有效的工具之一。从现阶段的主流体系结构讲,指令集可分为CISC和RISC两种。

CISC是“复杂指令集计算机”的缩写:Complex Instruction Set Computer。我们日常使用的 X86 就是 CISC 的典型代表。

计算机处理器包含有实现各种功能的指令或微指令,指令集越丰富,为微处理器编写程序就越容易,但是丰富的微指令集会影响其性能。复杂指令集计算机(CISC)体系结构的设计策略是使用大量的指令,包括复杂指令。与其他设计相比,在CISC中进行程序设计要比在其他设计中容易,因为每一项简单或复杂的任务都有一条对应的指令。程序设计者不需要写一大堆指令去完成一项复杂的任务。 但指令集的复杂性使得CPU和控制单元的电路非常复杂。[1]

RISC是“精简指令集计算机”的缩写:Reduced Instruction Set Computing RISC)。RISC的指令系统相对简单,它只要求硬件执行很有限且最常用的那部分指令,大部分复杂的操作则使用成熟的编译技术,由简单指令合成。RISC结构采用精简的,长短划一的指令集,使大多数的操作获得了尽可能高的效率。某些在传统结构中要用多周期指令实现的操作,在RISC结构中,通过机器语言编程,就代之以多条单周期指令了。【参考1】

接下来用一个例子来说明二者的区别。

假设我们有一个这样的计算机:内存是一个 6×4 的数组, 有A-F 6个寄存器。

对于 CISC 来说,如果想完成内存中2个数值的相乘的操作,可以直接使用下面的指令:

MULT 2:3,5:2

这里的 MULT 就是一个“复杂指令”。 进一步,如果设定变量 x 为内存 2:3 中的值。 y 为内存 5:2 的值,上面的指令就可以理解为C语言的 x=x*y。

如果用 RISC 来完成,那么需要写成如下的指令:

LOAD A, 2:3
LOAD B, 5:2
PROD A, B
STORE 2:3, A

从这个结果上来看,RISC 效率更低,因为需要执行更多的代码,使用更多的内存。但是实际上 RISC 的指令通常只需要1个机器周期来完成,所以整体上消耗时间和CISC 的 MULT 指令相同,另外因为 RISC 机器码指令长度相同,执行时只需要一个机器周期所以更适合流水线方式执行(我的理解是方便硬件预读取和判断)。【参考2】

虽然 Intel X86 CPU 是 CISC 的典型代表,但是它的内部也存在RISC 的设计概念。所有的操作都会被分解为微指令再执行。微指令是典型的 CISC 的指令。

微指令(英语:microcode,μop),还有翻译为微码或者微操作,为了避免歧义,后面全部使用 μop,是在CISC结构下,运行一些功能复杂的指令时,所分解一系列相对简单的指令。相关的概念最早在1947年开始出现。
微指令的作用是将机器指令与相关的电路实现分离,这样一来机器指令可以更自由的进行设计与修改,而不用考虑到实际的电路架构。与其他方式比较起来,使用微指令架构可以在降低电路复杂度的同时,建构出复杂的多步骤机器指令。撰写微指令一般称为微程序设计(microprogramming),而特定架构下的处理器实做中微指令有时会称为微程序(microprogram)。


现代的微指令通常由CPU工程师在设计阶段编写,并且存储在只读内存(ROM, read-only-memory)或可编程逻辑数组(PLA, programmable logic array)中。然而有些机器会将微指令存储在静态随机存取内存(SRAM)或是闪存(flash memory)中。它通常对普通程序员甚至是汇编语言程序员来说是不可见的,也是无法修改的。与机器指令不同的是,机器指令必须在一系列不同的处理器之间维持兼容性,而微指令只设计成在特定的电路架构下运行,成为特定处理器设计的一部分。【参考3】

参考:
1.https://baike.baidu.com/item/%E7%B2%BE%E7%AE%80%E6%8C%87%E4%BB%A4%E9%9B%86%E8%AE%A1%E7%AE%97%E6%9C%BA/661859?fromtitle=risc&fromid=62696#viewPageContent

2.https://cs.stanford.edu/people/eroberts/courses/soco/projects/risc/risccisc/

3.https://baike.baidu.com/item/%E5%BE%AE%E7%A0%81/10708310?fr=aladdin

3.http://www.enroo.com/support/category1/dpjrmzs/78378500.html 什么是RISC架构?RISC架构的优点与缺点

4.http://www.enroo.com/support/category1/dpjrmzs/38700314.html 什么是CISC体系结构?CISC架构的优点与缺点

5.https://teachcomputerscience.com/risc-and-cisc-processors/ RISC and CISC Processors

6.https://www.zhihu.com/question/404743266/answer/1321179351 这里还有一个 X86 为什么选择 CISC 的历史。

CISC诞生的原因也很简单,第一,当时的主流看法是设计硬件比设计编译器简单;第二,而且当时的内存很贵,以8086开始设计的1976年来看,当时4KB内存需要159美元,8KB内存需要250美元;第三,当时的内存速度很慢,而寄存器价格上天,8086只有约20000个晶体管,而寄存器为了存储16bit数据就花费了至少256个晶体管。

8086的祖宗8008只有3500个晶体管,配合0.5Mhz的时钟频率,连寄存器-寄存器的复制都需要20us,更别说当时的内存速度了。

8008时期的DRAM每字节高达2.52美元,实现一个完整的系统就需要几千字节,基于各种原因,微架构工程师只能让处理器在使用同样的资源下尽可能的多干一些活,让一个指令干完全部工作,在RISC出来之后这种方式就被叫做复杂指令集了(CISC)

在70年代后期,随着编译器的普及和汇编的减少,正交寻址几乎被程序员忽略,虽然当时的编译器不能完全利用CISC处理器的优势,但是显然历史的车轮是不会停下的,于是乎正交寻址变得更加没用了。

实际上随着集成电路的发展,一些复杂指令集相比一系列简单的指令集更慢,因为芯片越来越复杂,但是设计者显然没有时间针对几百条指令的每一条都进行优化。几乎就在同时,得益于半导体工艺的进步,微处理器的运行速度变得比内存更快,而且可以预见的是在未来这个差距会变得越来越大,因此就需要腾出空间去安排更多的寄存器和缓存。

FireBeetle USB KB/MS Shield

USB 键盘鼠标是最常见的输入设备了,之前如果想在 Arduino 上使用这两种设备,一个可行的方法是使用 USB Host Shield。这种方法的缺点是:成本比较高(MAX3421E芯片贵),操作复杂(SPI接口),资料少。因为这样缺点的存在,USB Host Shield 在使用上存在诸多不便。偶然间看到USB键鼠转串口通讯控制芯片CH9350(南京沁恒,WCH,阅读过我文章的朋友都知道CH340 也是他们家的产品)。这款能够将 USB 键盘鼠标的有效信息转为串口数据。芯片特点如下:

  • 支持12Mbps全速USB传输和1.5Mbps低速USB传输,兼容USB V2.0。
  • 上位机端USB端口符合标准HID类协议,不需要额外安装驱动程序,支持内置HID类设备驱动的Windows、Linux、MAC等操作系统。
  • 同一芯片可配置为上位机模式和下位机模式,分别连接USB-Host主机和USB键盘、鼠标。
  • 支持USB键盘鼠标在BIOS界面使用,支持多媒体功能键,支持不同分辨率USB鼠标。
  • 支持各种品牌的USB键盘鼠标、USB无线键盘鼠标、USB转PS2线等。
  • 上位机端和下位机端支持热插拔。
  • 提供发送状态引脚,支持485通讯。
  • 串口支持115200/57600/38400串口通信波特率。
  • 内置晶振和上电复位电路,外围电路简单。
  • 支持5V、3.3V电源电压。
  • 提供LQFP-48无铅封装,兼容RoHS。

于是尝试给 ESP32 FireBeetle 绘制一个  USB Keyboard/Mouse Shield,让 ESP32 能够轻松的获得 USB 键盘鼠标数据。

第一步,设计电路。核心是 CH9350芯片,它能够一次性支持2个 USB Host接口,下图中的 USB1 和 USB2。LED1 和 LED2 是通讯指示灯,对应的 USB1 和 USB2 如果有正常的通讯,对应的 LED会熄灭。此外,还有一个USB_Power 是USB公头,用于从外部取电,避免 FireBeetle 供电不足的情况。

CH9350L 最小系统

PCB 设计如下:

CH9350L PCB

3D 预览如下:

做出来就是这样(美中不足的因为2个USB 母头的存在,这个板子稍微大一些。另外,这个芯片引脚比较密集,焊接费了一些功夫,如果你对自己焊接技术不放心,推荐直接 SMT 避免手工焊接):

因为芯片使用串口输出,所以很容易就能够获得数据。数据格式在 Data Sheet 上有描述:

CH9350L 输出数据格式

解析数据会出现在 FireBeetle Serial2上。

电路图是立创 EDA设计的,如下:

对应的 CH9350L Datasheet

再读 eSPI

5年前,我给出过如果有可能尽量不要在设计上使用 eSPI 接口,时至今日,至少在平板和笔记本上 eSPI 已经完全取代了 LPC 接口, 我们需要对此有所了解。

首先 eSPI 相比之前的 LPC 总线,有着速度更快的优势。最新的 ADL-P 平台上,eSPI 可达50Mhz (最多支持4个数据线,一次性可以传输 4Bits).

此外,还提供了虚拟信号的功能,比如,可以从eSPI上发送 SMI# 信号,这样无需外部有SMI# 的引线即可让 EC 发送 SMI# 请求。从另外的角度来说,使用 eSPI 之后可以简化布线节省 PCB 空间。

再有就是提供通过 PECI 读取CPU /PCH 温度这样的功能,便于 EC 进行温度方面的控制。这样可以实现更好的散热功能。

传统的设计上, BIOS 和 EC 的 Firemware 是分别存储在两个 SPI NOR 中的:

使用 eSPI 之后可以共享。共享方式有如下两种:

第一种:挂接在 PCH 上,EC 访问需要通过 PCH 的 eSPI Master, 称作 Master Attached Flash Sharing (简称 MAFS,或者 MAF),这种模式下 EC 通过PCH 中的SPI Master 来访问存放在 SPI Nor 中的 EC Firmware。

MAFS

特别注意的是,还有一种 MAFS 的变种 G3 Flash Sharing,特别之处在于 EC 和 PCH 都是直连 SPI Nor。在PCH Reset 之前,EC 作为 SPI master 需要完成读取 Firmware 的动作。PCH Reset 之后,PCH 作为 SPI Master 来和 SPI Nor 进行通讯。G3 Flash Sharing 的设定和CSME以及BIOS 无关,完全是硬件动作,所以你也无法在 FIT 中找到设定的选项(个人非常不建议用这个模式,因为出现问题不容易判断产生错误的原因,可能是硬件也可能是 EC Firmare) 随着时代的进步,G3 Sharing 已经成为主流,现在建议跟随 Intel 参考设计进行:

第二种,SPI 挂在 EC 下面,PCH 访问需要透过EC, 这种称作 Slave Attached Flash Sharing(简称 SAFS,或者 SAF)

SAFS

个人建议:尽量不要使用共享模式,虽然能帮老板省一点钱,但是可能会有未知的问题。或者说参考板用哪种,尽量用哪种。特别是新的 Chipset ,理论上全部都可以支持,但是很可能非参考板的设计目前尚未验证过,如果想让它正常工作还需要特别的PMC 之类的设定。

参考:

1.https://blog.csdn.net/gkcywbcbjpvel404/article/details/107401736

2.Intel: Enhanced SPI (eSPI) Platform Enabling and Debug Guide

ESP32 的 Software Serial 库

ESP32 支持3个串口,ESP32 S2支持2个串口。但是,你终究会遇到需求比硬件支持多一个的情况。这种情况下就需要使用软串口。

这里推荐一个 ESP32 SoftSerial ,经过我的测试可以在ESP32 S2 上正常使用。

示例代码如下,使用 GPIO12 发送,未使用接收功能,比如,我们需要多出来的串口输出调试信息:
#include <SoftwareSerial.h>
                   // RX TX
SoftwareSerial swSer(SW_SERIAL_UNUSED_PIN, 12, false, 256);

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

  Serial.println("\nSoftware serial test started");

  for (char ch = ' '; ch <= 'z'; ch++) {
    swSer.write(ch);
  }
  swSer.println("");

}

void loop() {
  while (swSer.available() > 0) {
    Serial.write(swSer.read());
  }
  while (Serial.available() > 0) {
    swSer.write(Serial.read());
  }

}

库文件:

来自:https://github.com/akshaybaweja/SoftwareSerial

VC 重复宏定义 Warning

如果我们在代码中重复定义一个宏(macro redefine),例如:

#define SUM(a,b) a+b
#define SUM(a,b) a*b

会遇到下面的错误提示:

MRedef.c(20): error C2220: warning treated as error - no 'object' file generated
MRedef.c(20): warning C4005: 'SUM': macro redefinition
MRedef.c(19): note: see previous definition of 'SUM'

解决方法是在文件头部加入 Disable 这个 Warning 的指令如下:

#pragma warning (disable : 4005)

完整的代码如下:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>

#pragma warning (disable : 4005)

/*
MRedef.c(20): error C2220: warning treated as error - no 'object' file generated
MRedef.c(20): warning C4005: 'SUM': macro redefinition
MRedef.c(19): note: see previous definition of 'SUM'
*/

#define SUM(a,b) a+b
#define SUM(a,b) a*b


/***
  Print a welcoming message.

  Establishes the main structure of the application.

  @retval  0         The application exited normally.
  @retval  Other     An error occurred.
***/
INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  )
{
        int c =3,d=4;

  Print(L"Hello there fellow Programmer.\n");
  Print(L"Welcome to the world of EDK II.\n");

  Print(L"Macro test %d\n",SUM(c,d));
  
  return(0);
}

参考:

1.https://stackoverflow.com/questions/25201724/fix-macro-redefinition-in-c

Step to UEFI (234)不定参数函数的测试

一些情况下,调用函数的参数并不确定,显而易见的一个例子是 printf,在调用的时候后面可能接多个参数,对于这种情况我们可以使用 VA_LIST 来解决。在 Base.h 中有定义:

//
//  Support for variable argument lists in freestanding edk2 modules.
//
//  For modules that use the ISO C library interfaces for variable
//  argument lists, refer to "StdLib/Include/stdarg.h".
//
//  VA_LIST  - typedef for argument list.
//  VA_START (VA_LIST Marker, argument before the ...) - Init Marker for use.
//  VA_END (VA_LIST Marker) - Clear Marker
//  VA_ARG (VA_LIST Marker, var arg type) - Use Marker to get an argument from
//    the ... list. You must know the type and pass it in this macro.  Type
//    must be compatible with the type of the actual next argument (as promoted
//    according to the default argument promotions.)
//  VA_COPY (VA_LIST Dest, VA_LIST Start) - Initialize Dest as a copy of Start.
//
//  Example:
//
//  UINTN
//  EFIAPI
//  ExampleVarArg (
//    IN UINTN  NumberOfArgs,
//    ...
//    )
//  {
//    VA_LIST Marker;
//    UINTN   Index;
//    UINTN   Result;
//
//    //
//    // Initialize the Marker
//    //
//    VA_START (Marker, NumberOfArgs);
//    for (Index = 0, Result = 0; Index < NumberOfArgs; Index++) {
//      //
//      // The ... list is a series of UINTN values, so sum them up.
//      //
//      Result += VA_ARG (Marker, UINTN);
//    }
//
//    VA_END (Marker);
//    return Result;
//  }
//
//  Notes:
//  - Functions that call VA_START() / VA_END() must have a variable
//    argument list and must be declared EFIAPI.
//  - Functions that call VA_COPY() / VA_END() must be declared EFIAPI.
//  - Functions that only use VA_LIST and VA_ARG() need not be EFIAPI.
//

根据上面的代码,编写测试例子如下:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>

UINTN
EFIAPI
VarSum (
  IN UINTN  NumberOfArgs,
  ...
  )
{
  VA_LIST Marker;
  UINTN   Index;
  UINTN   Result;

  //
  // Initialize the Marker
  //
  VA_START (Marker, NumberOfArgs);
  for (Index = 0, Result = 0; Index < NumberOfArgs; Index++) {
    //
    // The ... list is a series of UINTN values, so sum them up.
    //
    Result += VA_ARG (Marker, UINTN);
  }

  VA_END (Marker);
  return Result;
}

UINTN
EFIAPI
VarString (
  IN UINTN  NumberOfArgs,
  ...
  )
{
  VA_LIST Marker;
  UINTN   Index;
  UINTN   Result;

  //
  // Initialize the Marker
  //
  VA_START (Marker, NumberOfArgs);
  for (Index = 0, Result = 0; Index < NumberOfArgs; Index++) {
    //
    // The ... list is a series of UINTN values, so sum them up.
    //
    //Result += VA_ARG (Marker, UINTN);
    Print(L"String %d: %s\n",Index,VA_ARG (Marker, CHAR16*));
  }

  VA_END (Marker);
  return Result;
}

/***
  Print a welcoming message.

  Establishes the main structure of the application.

  @retval  0         The application exited normally.
  @retval  Other     An error occurred.
***/
INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  )
{

  Print(L"VarSum(2,1,2)=%d\n",VarSum(2,1,2));
  Print(L"VarSum(3,1,2,3)=%d\n",VarSum(3,1,2,3));
  Print(L"VarSum(4,1,2,3,4)=%d\n",VarSum(4,1,2,3,4));
  
  Print(L"VarString(4,L\"abcde\",L\"1234\",L\"efgh\",L\"5678\")\n",
                VarString(4,L"abcde",L"1234",L"efgh",L"5678"));
  return(0);
}

运行结果:

不定参数函数运行结果

可以看到VarSum()能够实现将可变个数值相加,VarString()能够实现在屏幕输出可变个字符串变量的功能。

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 修改工具可以在这里下载: