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):

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

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;
        }

    }
}