使用 CH567 USB 键盘增加支持 HID_SETREPORT

从插入USB 键盘开始抓取数据,按笔记本键盘上的按键,测试 Num 从灭到亮,再到灭;测试 Caps 从灭到亮,再到灭。其中的 SET_REPORT 有三个:

  1. 是刚插入的时候系统用来通知键盘,告知当前三个 LED 状态的;
  2. 是NUM LED 从灭到亮,再到灭的动作;
  3. 是CAPS LED 从灭到亮,再到灭的动作;

首先刷上之前的键盘固件,使用逻辑分析仪分析,可以看到,我们之前的设备没有处理SET_REPORT所以会用 STALL 回复:

因此,我们需要响应这个命令。下面是正常情况下,对于一个 SET_REPORT 的通讯:

此外,测试了按USB 键盘 CAPS LOCK按键,让灯从灭到亮再灭的过程。可以看出,USB 键盘通知系统当前有 CAPS LOCK按键,然后系统再通过 HID_REPORT通知当USB键盘前应该的灯状态来实现的。

  1. 首先在USB1Dev_EDP0_Setup_Deal函数中运行处理 Setup包的代码

// 处理 HID_SET_REPORT

                        if ((UsbSetupBuf->bRequestType==0x21)&&(SetupReqCode==HID_SET_REPORT))

                        {

                                CurrentRequest=HID_SET_REPORT;

                                printf(“SETUP HID_SET_REPORT”);

                                return;

                        }

  • 接下来在USB1Dev_EDP0_OUT_Deal函数中处理 Endpoint0 OUT 数据

        if (CurrentRequest==HID_SET_REPORT)

        {

                printf(“EDP0_OUT HID_SET_REPORT:”);

                for (i=0; i<len; i++)

                {

                        printf(“%X “,UsbEp1OUTBuf[i]);

                }

                printf(“\n”);

        }

  • 最后在USB1Dev_EDP0_IN_Deal函数中发送0字节 Package

        if (CurrentRequest==HID_SET_REPORT)

        {

                R16_UEP0_T_LEN1 = 0;

                R8_UEP0_TX_CTRL1 = UEP_DATA1 | UEP_T_RES_ACK;

                printf(“EDP0_IN HID_SET_REPORT\n”);

                CurrentRequest=0;

                return;

        }

最终成功的代码:

完整的代码:

Step to UEFI (249)继续在PeiMain 中(8)

第一次进入PeiCore() 的时候,因为环境尚未准备好,所以很快就能执行到下面的代码:

  //
  // Call PEIM dispatcher
  //
  PeiDispatcher (SecCoreData, &PrivateData);

在这里会执行 PEIM的执行:

MdeModulePkg\Core\Pei\Dispatcher\Dispatcher.c
/**
  Conduct PEIM dispatch.

  @param SecCoreData     Points to a data structure containing information about the PEI core's operating
                         environment, such as the size and location of temporary RAM, the stack location and
                         the BFV location.
  @param Private         Pointer to the private data passed in from caller

**/
VOID
PeiDispatcher (
  IN CONST EFI_SEC_PEI_HAND_OFF  *SecCoreData,
  IN PEI_CORE_INSTANCE           *Private
  )

进去这个函数之后,因为刚开始内存并没有准备好,所以 (Private->PeiMemoryInstalled) 这个条件不会满足,会进入下面的do … while 循环。

  //
  // This is the main dispatch loop.  It will search known FVs for PEIMs and
  // attempt to dispatch them.  If any PEIM gets dispatched through a single
  // pass of the dispatcher, it will start over from the BFV again to see
  // if any new PEIMs dependencies got satisfied.  With a well ordered
  // FV where PEIMs are found in the order their dependencies are also
  // satisfied, this dispatcher should run only once.
  //

跳转到DiscoverPeimsAndOrderWithApriori() 函数中

      if (Private->CurrentPeimCount == 0) {
        //
        // When going through each FV, at first, search Apriori file to
        // reorder all PEIMs to ensure the PEIMs in Apriori file to get
        // dispatch at first.
        //
        DiscoverPeimsAndOrderWithApriori (Private, CoreFvHandle);
      }

这个函数的作用是找到所有的 PEIM以及Apriori 文件。

/**

  Discover all PEIMs and optional Apriori file in one FV. There is at most one
  Apriori file in one FV.


  @param Private          Pointer to the private data passed in from caller
  @param CoreFileHandle   The instance of PEI_CORE_FV_HANDLE.

**/
VOID
DiscoverPeimsAndOrderWithApriori (
  IN  PEI_CORE_INSTANCE    *Private,
  IN  PEI_CORE_FV_HANDLE   *CoreFileHandle
  )

函数中有一个 DEBUG 输出如下:

  DEBUG ((
    DEBUG_INFO,
    "%a(): Found 0x%x PEI FFS files in the %dth FV\n",
    __FUNCTION__,
    PeimCount,
    Private->CurrentPeimFvCount
));

对应的串口 Log 中有如下字样,就是说在找到了7个 PEIM

DiscoverPeimsAndOrderWithApriori(): Found 0x7 PEI FFS files in the 0th FV

为了更好的观测,加入代码,输出它找到的 PEIM 的 GUID:

      for (Index = 0; Index < PeimCount; Index++) {
        //
        // Make an array of file name GUIDs that matches the FileHandle array so we can convert
        // quickly from file name to file handle
        //
        Status = FvPpi->GetFileInfo (FvPpi, TempFileHandles[Index], &FileInfo);
        ASSERT_EFI_ERROR (Status);
        CopyMem (&TempFileGuid[Index], &FileInfo.FileName, sizeof(EFI_GUID));
		DEBUG ((DEBUG_INFO , "%g\n", FileInfo.FileName));
      }

然后可以在串口输出中看到多了如下的输出:

9B3ADA4F-AE56-4C24-8DEA-F03B7558AE50
A3610442-E69F-4DF3-82CA-2360C4031A23
9D225237-FA01-464C-A949-BAABC02D31D0
222C386D-5ABC-4FB4-B124-FBB82488ACF4
86D70125-BAA3-4296-A62F-602BEBBB9081
89E549B0-7CFE-449D-9BA3-10D8B2312D71
EDADEB9D-DDBA-48BD-9D22-C1C169C8C5C6

使用工具继续查看,可以将上面的GUID 和文件名对应起来:

GUID文件名
9B3ADA4F-AE56-4C24-8DEA-F03B7558AE50PcdPeim.efi
A3610442-E69F-4DF3-82CA-2360C4031A23ReportStatusCodeRouterPei.efi
9D225237-FA01-464C-A949-BAABC02D31D0StatusCodeHandlePei.efi
222C386D-5ABC-4FB4-B124-FBB82488ACF4PlatformPei.efi
86D70125-BAA3-4296-A62F-602BEBBB9081DxeIpl.efi
89E549B0-7CFE-449D-9BA3-10D8B2312D71S3Resume2Pei.efi
EDADEB9D-DDBA-48BD-9D22-C1C169C8C5C6CpuMpPei.efi

特别注意,用工具我们能看到9个文件(模块),但是这里只提示找到7个。

接下来退出      DiscoverPeimsAndOrderWithApriori() 函数继续在PeiDispatcher 中执行

     //
      // Start to dispatch all modules within the current FV.
      //

对于每一个 FFS文件,需要用DepexSatisfied()检查是否满足条件,如果满足会使用PeiLoadImage()

              //
              // For PEIM driver, Load its entry point
              //
              Status = PeiLoadImage (
                         PeiServices,
                         PeimFileHandle,
                         PEIM_STATE_NOT_DISPATCHED,
                         &EntryPoint,
                         &AuthenticationState
                         );

可以通过加入 Log 的方法证明这里调用的是位于\MdeModulePkg\Core\Pei\Image\Image.c 中的函数

/**
  Routine to load image file for subsequent execution by LoadFile Ppi.
  If any LoadFile Ppi is not found, the build-in support function for the PE32+/TE
  XIP image format is used.

  @param PeiServices     - An indirect pointer to the EFI_PEI_SERVICES table published by the PEI Foundation
  @param FileHandle      - Pointer to the FFS file header of the image.
  @param PeimState       - The dispatch state of the input PEIM handle.
  @param EntryPoint      - Pointer to entry point of specified image file for output.
  @param AuthenticationState - Pointer to attestation authentication state of image.

  @retval EFI_SUCCESS    - Image is successfully loaded.
  @retval EFI_NOT_FOUND  - Fail to locate necessary PPI
  @retval Others         - Fail to load file.

**/
EFI_STATUS
PeiLoadImage (
  IN     CONST EFI_PEI_SERVICES       **PeiServices,
  IN     EFI_PEI_FILE_HANDLE          FileHandle,
  IN     UINT8                        PeimState,
  OUT    EFI_PHYSICAL_ADDRESS         *EntryPoint,
  OUT    UINT32                       *AuthenticationState
  )

其中通过如下调用来实现真正的加载:

      Status = LoadFile->LoadFile (
                          LoadFile,
                          FileHandle,
                          &ImageAddress,
                          &ImageSize,
                          EntryPoint,
                          AuthenticationState
                          );

实际的 Load动作是通过处于同一个文件中的PeiLoadImageLoadImageWrapper() 来实现的,而这个函数中实际干活的是PeiLoadImageLoadImage() 函数:

/**
  Loads a PEIM into memory for subsequent execution. If there are compressed
  images or images that need to be relocated into memory for performance reasons,
  this service performs that transformation.

  @param PeiServices      An indirect pointer to the EFI_PEI_SERVICES table published by the PEI Foundation
  @param FileHandle       Pointer to the FFS file header of the image.
  @param ImageAddressArg  Pointer to PE/TE image.
  @param ImageSizeArg     Size of PE/TE image.
  @param EntryPoint       Pointer to entry point of specified image file for output.
  @param AuthenticationState - Pointer to attestation authentication state of image.

  @retval EFI_SUCCESS      Image is successfully loaded.
  @retval EFI_NOT_FOUND    Fail to locate necessary PPI.
  @retval EFI_UNSUPPORTED  Image Machine Type is not supported.
  @retval EFI_WARN_BUFFER_TOO_SMALL 
                           There is not enough heap to allocate the requested size.
                           This will not prevent the XIP image from being invoked.

**/
EFI_STATUS
PeiLoadImageLoadImage (
  IN     CONST EFI_PEI_SERVICES       **PeiServices,
  IN     EFI_PEI_FILE_HANDLE          FileHandle,
  OUT    EFI_PHYSICAL_ADDRESS         *ImageAddressArg,  OPTIONAL
  OUT    UINT64                       *ImageSizeArg,     OPTIONAL
  OUT    EFI_PHYSICAL_ADDRESS         *EntryPoint,
  OUT    UINT32                       *AuthenticationState
  )

  其中有2处使用 DEBUG 宏进行串口输出的代码(这里研究对象是 EDK2 202108,不同版本之间可能有差别)

  DEBUG ((DEBUG_INFO, "Loading PEIM %g\n", FileHandle));
   DEBUG ((EFI_D_INFO | EFI_D_LOAD, "Loading PEIM at 0x%11p EntryPoint=0x%11p ", (VOID *)(UINTN)ImageAddress, (VOID *)(UINTN)*EntryPoint));

因此串口 Log 中看到的如下字样:

Loading PEIM 9B3ADA4F-AE56-4C24-8DEA-F03B7558AE50
Loading PEIM at 0x0000083D120 EntryPoint=0x0000083D620 PcdPeim.efi

接下的部分串口 Log就是来自PcdPeim.efi。

USB 键盘支持 HID_SET_IDLE

之前的代码中,没有对 SET_IDLE 进行处理,所以会使用 STALL 进行回复。这次的目标是在代码中添加处理的代码。

作为比对,正常情况下应该使用下面的方式处理:

                /* 分析并处理当前的SETUP包 */
                len = 0;                                                                      // 默认为成功并且上传0长度
                status = 0;
                if (( UsbSetupBuf->bRequestType &amp; USB_REQ_TYP_MASK ) != USB_REQ_TYP_STANDARD )  /* 非标准请求 */
                {
                        // 处理 HID_SET_IDLE
                        if ((UsbSetupBuf->bRequestType==0x21)&amp;&amp;(SetupReqCode==HID_SET_IDLE)) {
                                printf("SETUP HID_SET_IDLE\n");
                                R16_UEP0_T_LEN1 = 0;
                                R8_UEP0_TX_CTRL1 = UEP_DATA1 | UEP_T_RES_ACK;                    // 默认数据包是DATA1
                                return ;
                        }
                        status = 0xFF;  // 操作失败
                }

修改之后的运行结果:

完整代码如下:

CSME 简述

CSME 算是 Intel X86 PC 上最神秘的部分了,本文根据 us-19-Hasarfaty-Behind-The-Scenes-Of-Intel-Security-And-Manageability-Engine 一文写成。讲述内容无法证伪,各位随便听听即可,了解这些能够帮助BIOS 工程师更好的理解一些操作的实现。文章基于 Intel 第八代第九代CPU(Coffee Lake 和 Whiskey Lake)进行描述。

第一个问题,什么是 CSME? 这是 Converged Security and Manageability Engine  (我翻译成“集中式安全管理引擎”)的缩写 ,或者更简单称作 ME(对于一些小核系统,因为没有管理功能(Manageability),所以会缩写成 CSE,但是我并不建议这样做,因为容易引起误解) :

我的笔记本电脑上,设备管理器中可以看到这个设备。

第二个问题,CSME提供了什么功能。

从图中可以看到 CSME 位于PCH 中,对外可以通过 LAN 和 WIFI 直接通讯

从资料上看,引入这个设备可以提供如下功能:

  1. 对当前系统提供安全保护。比如, BootGuard 功能;
  2. 频率控制。比如,我们在 FIT中可以看到一些关于 PCIE Clock Request 之类的设定。上电之后CSME 先于CPU运行,因此一些时钟信号可以由 CSME 准备好,之后再提供给 CPU;
  3. 加载一些Patch给 PCH/CPU;
  4. 提供一些运行期的安全服务,比如:TPM ,DRM(Digital Rights Management,数字版权保护技术,能够阻止在未经授权的机器上播放带有版权保护的视频),DAL(Intel® Dynamic Application Loader,能够将Java 代码放在 CSME 中执行,这样能够更好的防止被破解【参考2】)
  5. AMT (Intel Active Management Technology, 英特尔主动管理技术), 这是企业版 PCH 才有的功能,打开这个功能后下,配合部署的管理软件能够实现一些远程管理功能。比如,通过 Gbe提供的网络功能远程登陆查看当前屏幕信息。理论上有了这个功能,只要能开机IT工程师就可以远程控制你的电脑来完成装系统等等工作,我见过实验室测试这样的功能,但从未见过有人使用这样的功能进行系统维护。同样的 Vpro 也是基于这个功能。因为这个功能能够“远程开机”和“监视屏幕”,所以这个功能经常被认为是 Intel 后门,但实际上这个是预留给局域网管理员的管理功能,好比:远程桌面或者TeamViewer 这种软件。另外,如果用过这个功能的人都会知道,当它进行屏幕监视的时候,会有大大的提示信息。

第三个问题:CSME 的硬件架构。

上图中最右侧CSME 内部有一个独立的 CPU (可以用 EC 进行类比),是一个32位 Intel 486 级别的CPU。SRAM 是独立于系统内存,有 1.5MB左的大小。正是因为有独立 CPU 和 SRAM 的存在,所以 CSME 能够监控CPU。此外们还有 ROM ,里面存放着固化的内容,是整个信任链的根节点。

System Agent 提供了CSME 和外部进行数据交换的接口。

OCS (Offload & Cryptography Subsystem):提供了加密算法的加速硬件,同时存放一些安全相关的密钥。比如,Secure Boot 的Private Key 之类都是存放在这里的。

Gasket 提供了对 PCH fabric 的接口和 CSME 的 IO 接口,例如:TPM 和 HECI (Management Engine Interface),最前面我设备管理器中可以看到有一个 HECI Controller。

Manageability Devices:用于控制管理和重定向设备,比如,你通过网络访问这个电脑,那么需要将键盘鼠标重定向到网络上。

Protected Real Time Clock: 带有保护的时钟。很多时候,需要从时间之类生成随机种子,如果从外部获取时间,很容易被欺骗,因此这里有一个内置的时钟。

从上面可以看到,当我们谈论烧写在主板SPINOR 的时候,可能提到的是 IFWI (Integrated FirmWare Image),就是我们使用 FIT 打开的那个 Image,也可能是 BIOS 本身(或者说 IFWI 中的 BIOS Region)。一般情况下,当我们使用 SF100 这样的工具进行烧写时,烧录上的是  IFWI (SF100无法区分具体哪个是 ME 哪个部分是BIOS Region,所以是完全擦写的),而当我们在 Windows 下或者 Shell 下烧写时,有可能是 IFWI 或者是BIOS, 软件本身有能力区分具体要求。所以,某天你给别人提供Image 时,最好明确你提供的是 IFWI 还是 BIOS Image。

在中文的世界中,“辐射”这个词有多种含义,比如:电磁辐射和电离辐射。前者无处不在,能够帮助我们看到东西,能够帮助我们进行通讯。后者也能帮助我们看到肉眼无法看到的东西,比如:X光测试,甚至之前的CRT显示器内部也存在着这种“辐射”。正是因为这样的误解,使得手机基站在很多小区成为大妈喊打的东西。前面提到的CSME 的“监控”也是这样,更多时候只能查看CPU 的状态,但是在很多人眼中它能够“监视”和“控制”用户的数据—-不过至少现在我还没有看到这样的实例。

参考:

1. https://software.intel.com/content/www/cn/zh/develop/tools/dal/overview.html

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

2024年1月2日 有兴趣可以阅读来自 Intel 官方的文档

https://www.intel.com/content/dam/www/public/us/en/security-advisory/documents/intel-csme-security-white-paper.pdf

Intel BIOS 测试职位

Job Title

Firmware/BIOS Quality Engineer

Job Responsibility:

In this position, you will be part of the Firmware Integration and Validation (FIV) Team within SATG responsible for UEFI Firmware/BIOS quality assurance on Intel server platforms. Behavioral traits for this position include: validation infrastructure and test case development, in-depth bug investigation and fix, good communication and problem solving skills, multi-tasking, self-motivation and the ability to collaborate with internal/external stakeholders.

The candidate’s responsibilities will include but not limit to:

  • Design and develop framework and utility for IP/module level validation according to module specifications, such as PRD, HAS etc;
  • Be responsible for test case development according to product requirement for server BIOS;
  • Be responsible for the test execution on Intel server platform for BIOS and platform FW test;
  • Work with team members to investigate test failures using variety of debug tools to perform evaluation of the issue and identify the failure to component or code level;
  • Be responsible for developing and sustaining the automation test framework, test scripts and utilities; deploy and maintain automation test cases, analyze test reports and logs.

Qualifications:

The candidate should possess a Master or a Bachelor degree in computer science, Computer Engineering or a related field, with at least 5 years industrial experience on BIOS development or testing. Additional qualifications include:

  • Solid knowledge of BIOS and UEFI Firmware;
  • Deep understanding of server architecture of both HW and SW;
  • Deep understanding of industry specification such as UEFI/ACPI/SMBIOS/TPM/etc;
  • Software development skills with different programming languages include but not limited to: C/C++/C#, Python, Windows Batch Script, JavaScript, etc;
  • Solid experience of software testing process and methodologies;
  • Skilled in BIOS test case development and issue debugging;
  • Good English communication skill in reading and writing, good speaking capability;
  • Knowledge of Linux Kernel or device driver development is a plus.

有兴趣的朋友可以直接联系:jinghua.xu@intel.com

Force Memory Traing 工具

除了SPD 中给出的参数,在内存初始化时,CPU 还需要有针对性的获得一些参数,而这些参数和生产制造环境温度等等情况有关系(听起来时玄学)。因此,在开机的时候内存控制器会进行一个 Memory Training 的动作。比如,当前内存槽上有2根内存,但是因为线长的原因,同样的信号到达内存的时间不同。在开机的时候,内存控制器就发出一个信号分别给两个内存,内存收到信号后发送一个应答。根据回复的时间不同,内存控制器可以取得一个能够让两个内存都工作正常的时序。当然,这里只是非常粗略的介绍,实际上这个过程非常复杂,耗时也会很久。而Training完成后,BIOS会将取得的参数保存在SPI NOR 上。这也是为什么第一次刷过BIOS之后开机要很久的原因。

一些情况下,我们希望强制进行 Memory Training,通常的方法是进行 Clear CMOS。这次提供了一个 Windows 工具,运行之后重启,SoC 即会进行完整的 Memory Training。

适用范围: ADL-P/M 平台的设备

使用 CH567 制作一个USB 键盘

这一次使用 CH567 制作一个USB键盘,参考的对象是Dostyle 的MK60 机械键盘。

Dostyle MK60 机械键盘

同样的,使用 USBlyzer 抓取描述符信息:

USB Composite Device

Connection StatusDevice connected
Current Configuration1
SpeedFull (12 Mbit/s)
Device Address8
Number Of Open Pipes2

Device Descriptor Gaming KB

OffsetFieldSizeValueDescription
0bLength112h
1bDescriptorType101hDevice
2bcdUSB20110hUSB Spec 1.1
4bDeviceClass100hClass info in Ifc Descriptors
5bDeviceSubClass100h
6bDeviceProtocol100h
7bMaxPacketSize0108h8 bytes
8idVendor2258Ah
10idProduct2002Ah
12bcdDevice21201h12.01
14iManufacturer101h“SINO WEALTH”
15iProduct102h“Gaming KB “
16iSerialNumber100h
17bNumConfigurations101h

Configuration Descriptor 1 Bus Powered, 500 mA

OffsetFieldSizeValueDescription
0bLength109h
1bDescriptorType102hConfiguration
2wTotalLength2003Bh
4bNumInterfaces102h
5bConfigurationValue101h
6iConfiguration100h
7bmAttributes1A0hBus Powered, Remote Wakeup
4..0: Reserved…00000 
5: Remote Wakeup..1….. Yes
6: Self Powered.0…… No, Bus Powered
7: Reserved (set to one)
(bus-powered for 1.0)
1……. 
8bMaxPower1FAh500 mA

Interface Descriptor 0/0 HID, 1 Endpoint

OffsetFieldSizeValueDescription
0bLength109h
1bDescriptorType104hInterface
2bInterfaceNumber100h
3bAlternateSetting100h
4bNumEndpoints101h
5bInterfaceClass103hHID
6bInterfaceSubClass101hBoot Interface
7bInterfaceProtocol101hKeyboard
8iInterface100h

HID Descriptor

OffsetFieldSizeValueDescription
0bLength109h
1bDescriptorType121hHID
2bcdHID20111h1.11
4bCountryCode100h
5bNumDescriptors101h
6bDescriptorType122hReport
7wDescriptorLength20043h67 bytes

Endpoint Descriptor 81 1 In, Interrupt, 1 ms

OffsetFieldSizeValueDescription
0bLength107h
1bDescriptorType105hEndpoint
2bEndpointAddress181h1 In
3bmAttributes103hInterrupt
1..0: Transfer Type……11 Interrupt
7..2: Reserved000000.. 
4wMaxPacketSize20008h8 bytes
6bInterval101h1 ms

Interface Descriptor 1/0 HID, 1 Endpoint

OffsetFieldSizeValueDescription
0bLength109h
1bDescriptorType104hInterface
2bInterfaceNumber101h
3bAlternateSetting100h
4bNumEndpoints101h
5bInterfaceClass103hHID
6bInterfaceSubClass100h
7bInterfaceProtocol100h
8iInterface100h

HID Descriptor

OffsetFieldSizeValueDescription
0bLength109h
1bDescriptorType121hHID
2bcdHID20111h1.11
4bCountryCode100h
5bNumDescriptors101h
6bDescriptorType122hReport
7wDescriptorLength200CCh204 bytes

Endpoint Descriptor 82 2 In, Interrupt, 1 ms

OffsetFieldSizeValueDescription
0bLength107h
1bDescriptorType105hEndpoint
2bEndpointAddress182h2 In
3bmAttributes103hInterrupt
1..0: Transfer Type……11 Interrupt
7..2: Reserved000000.. 
4wMaxPacketSize20010h16 bytes
6bInterval101h1 ms

Interface 0 HID Report Descriptor Keyboard

Item Tag (Value)Raw Data
Usage Page (Generic Desktop)05 01 
Usage (Keyboard)09 06 
Collection (Application)A1 01 
    Usage Page (Keyboard/Keypad)05 07 
    Usage Minimum (Keyboard Left Control)19 E0 
    Usage Maximum (Keyboard Right GUI)29 E7 
    Logical Minimum (0)15 00 
    Logical Maximum (1)25 01 
    Report Count (8)95 08 
    Report Size (1)75 01 
    Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)81 02 
    Report Count (1)95 01 
    Report Size (8)75 08 
    Input (Cnst,Var,Abs,NWrp,Lin,Pref,NNul,Bit)81 03 
    Report Count (6)95 06 
    Report Size (8)75 08 
    Logical Minimum (0)15 00 
    Logical Maximum (255)26 FF 00 
    Usage Page (Keyboard/Keypad)05 07 
    Usage Minimum (Undefined)19 00 
    Usage Maximum2A FF 00 
    Input (Data,Ary,Abs)81 00 
    Logical Maximum (1)25 01 
    Report Count (5)95 05 
    Report Size (1)75 01 
    Usage Page (LEDs)05 08 
    Usage Minimum (Num Lock)19 01 
    Usage Maximum (Kana)29 05 
    Output (Data,Var,Abs,NWrp,Lin,Pref,NNul,NVol,Bit)91 02 
    Report Count (1)95 01 
    Report Size (3)75 03 
    Output (Cnst,Var,Abs,NWrp,Lin,Pref,NNul,NVol,Bit)91 03 
End CollectionC0 

Interface 1 HID Report Descriptor System Control

Item Tag (Value)Raw Data
Usage Page (Generic Desktop)05 01 
Usage (System Control)09 80 
Collection (Application)A1 01 
    Report ID (1)85 01 
    Usage Minimum (System Power Down)19 81 
    Usage Maximum (System Wake Up)29 83 
    Logical Minimum (0)15 00 
    Logical Maximum (1)25 01 
    Report Size (1)75 01 
    Report Count (3)95 03 
    Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)81 02 
    Report Count (5)95 05 
    Input (Cnst,Ary,Abs)81 01 
End CollectionC0 
Usage Page (Consumer Devices)05 0C 
Usage (Consumer Control)09 01 
Collection (Application)A1 01 
    Report ID (2)85 02 
    Usage Minimum (Undefined)19 00 
    Usage Maximum (AC Format)2A 3C 02 
    Logical Minimum (0)15 00 
    Logical Maximum (572)26 3C 02 
    Report Count (1)95 01 
    Report Size (16)75 10 
    Input (Data,Ary,Abs)81 00 
End CollectionC0 
Usage Page (Vendor-Defined 1)06 00 FF 
Usage (Vendor-Defined 1)09 01 
Collection (Application)A1 01 
    Report ID (5)85 05 
    Logical Minimum (0)15 00 
    Logical Maximum (255)26 FF 00 
    Usage Minimum (Vendor-Defined 1)19 01 
    Usage Maximum (Vendor-Defined 2)29 02 
    Report Size (8)75 08 
    Report Count (5)95 05 
    Feature (Data,Var,Abs,NWrp,Lin,Pref,NNul,NVol,Bit)B1 02 
End CollectionC0 
Usage Page (Generic Desktop)05 01 
Usage (Keyboard)09 06 
Collection (Application)A1 01 
    Report ID (6)85 06 
    Logical Minimum (0)15 00 
    Logical Maximum (1)25 01 
    Report Size (1)75 01 
    Report Count (112)95 70 
    Usage Page (Keyboard/Keypad)05 07 
    Usage Minimum (Keyboard Left Control)19 E0 
    Usage Maximum (Keyboard Right GUI)29 E7 
    Usage Minimum (Undefined)19 00 
    Usage Maximum (Keypad =)29 67 
    Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)81 02 
    Report Count (8)95 08 
    Input (Cnst,Ary,Abs)81 01 
End CollectionC0 
Usage Page (Vendor-Defined 1)06 00 FF 
Usage (Vendor-Defined 1)09 01 
Collection (Application)A1 01 
    Report ID (9)85 09 
    Logical Minimum (0)15 00 
    Logical Maximum (255)26 FF 00 
    Usage (Undefined)09 00 
    Report Size (8)75 08 
    Report Count (504)96 F8 01 
    Feature (Data,Var,Abs,NWrp,Lin,Pref,NNul,NVol,Bit)B1 02 
End CollectionC0 
Usage Page (Vendor-Defined 1)06 00 FF 
Usage (Vendor-Defined 1)09 01 
Collection (Application)A1 01 
    Report ID (10)85 0A 
    Logical Minimum (0)15 00 
    Logical Maximum (255)26 FF 00 
    Usage (Undefined)09 00 
    Report Size (8)75 08 
    Report Count (41)95 29 
    Feature (Data,Var,Abs,NWrp,Lin,Pref,NNul,NVol,Bit)B1 02 
End CollectionC0 
Usage Page (Vendor-Defined 1)06 00 FF 
Usage (Vendor-Defined 1)09 01 
Collection (Application)A1 01 
    Report ID (11)85 0B 
    Logical Minimum (0)15 00 
    Logical Maximum (255)26 FF 00 
    Usage (Undefined)09 00 
    Report Size (8)75 08 
    Report Count (126)95 7E 
    Feature (Data,Var,Abs,NWrp,Lin,Pref,NNul,NVol,Bit)B1 02 
End CollectionC0 
Usage Page (Vendor-Defined 1)06 00 FF 
Usage (Vendor-Defined 1)09 01 
Collection (Application)A1 01 
    Report ID (12)85 0C 
    Logical Minimum (0)15 00 
    Logical Maximum (255)26 FF 00 
    Usage (Undefined)09 00 
    Report Size (8)75 08 
    Report Count (1920)96 80 07 
    Feature (Data,Var,Abs,NWrp,Lin,Pref,NNul,NVol,Bit)B1 02 
End CollectionC0 

This report was generated by USBlyzer

需要特别注意的是:这个设备是 USB Full Speed(12Mbits/s)的,必须在文件头部设置为 Full Speed,如果继续使用 Low Speed,插入之后读取描述符后 Host就会停止继续发送数据(Windows 行为,我不知道原因)。

除此之外,和之前的鼠标代码相比还增加了一个HID 描述符,另外,在 Main 中定时发送打开Windows 菜单(Win按键)。

USB Host Shield 通过USB Hub 连接多个键盘

USB Host Shield 本身是支持USB Hub的,这样我们可以方便的一次性连接多个USB 设备。这次以USB 键盘为例进行测试。

USB Hub 的使用还是比较简单的,连接好之后,建议运行 USB_Host_Shield_Library_2.0\examples\hub_demo 这个示例代码进行测试,它会枚举当前USB Hub上的所有 USB 设备的描述符信息,通过这样的方法我们可以知道硬件是否能够工作正常。

前述代码测试通过后,我们就可以编写代码来获得按键信息了。

1. KBUnderHub.ino 代码中必须使用 USBHub ,之后,因为测试有3个键盘,所以要声明 kb1 到 kb3 三个设备

USB Usb;

USBHub Hub(&Usb);

KBSET kb1(&Usb);

KBSET kb2(&Usb);

KBSET kb3(&Usb);

2. KeyboardSets.h 代码中,申明一些我们需要的结构体如下:

// 当前支持的键盘数量
#define KBNUM 3

class KBSET : public HIDUniversal {
  public:
    KBSET(USB *p) : HIDUniversal(p) {};
    bool connected() {
      return HIDUniversal::isReady();
    };
    // 本次收到数据的键盘编号
    uint8_t  Current;
    // 存放第x个键盘是否有改变发生的标记
    bool     Changed[KBNUM];
    // 存放第x个键盘当前收到的缓冲区长度
    uint8_t  BufferSize[KBNUM];
    // 存放第x个键盘收到的缓冲区数据
    uint8_t  Buffer[KBNUM][64];
    // 存放第x个键盘的PID 和 VID
    uint16_t PID[KBNUM];
    uint16_t VID[KBNUM];
  private:
    void ParseHIDData(USBHID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf); // Called by the HIDUniversal library
    uint8_t  BufferSizeOld[KBNUM];
    uint8_t  BufferOld[KBNUM][64];
    // 当前收到的键盘数据总数
    uint8_t  TotolKB;
    uint8_t OnInitSuccessful() { // Called by the HIDUniversal library on success
      KBSET::TotolKB = 0;
      for (uint8_t j = 0; j < KBNUM; j++) {
        for (uint8_t i = 0; i < 64; i++) {
          KBSET::BufferOld[j][i]=0xFF;
        }
      }
      return 0;
    };
};

3. KeyboardSets.cpp

#include "KeyboardSets.h"

void KBSET::ParseHIDData(USBHID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf) {
  /*
    if (len && buf)  {
      Notify(PSTR("\r\n"), 0x80);
      for (uint8_t i = 0; i < len; i++) {
        D_PrintHex<uint8_t > (buf[i], 0x80);
        Notify(PSTR(" "), 0x80);
      }
    }
  */

  // 在 KBSET 中查找记录
  KBSET::Current = 0xFF;
  for (uint8_t i = 0; i < KBNUM; i++) {
    //
    if ((KBSET::PID[i] == HIDUniversal::PID) && (KBSET::VID[i] == HIDUniversal::VID)) {
      KBSET::Current = i;
    }
  }
  // 如果查找不到
  if (KBSET::Current == 0xFF) {
    KBSET::Current = KBSET::TotolKB;
    KBSET::PID[KBSET::Current] = HIDUniversal::PID;
    KBSET::VID[KBSET::Current] = HIDUniversal::VID;
    KBSET::BufferSize[KBSET::Current] = len;
    KBSET::TotolKB++;
  }

  // 检查本次数据和上次数据是否有差别
  if (memcmp(BufferOld[KBSET::Current], buf, len) == 0) {
    // 没有差别
    KBSET::Changed[KBSET::Current] = false;
  } else {
    memcpy(Buffer[KBSET::Current], buf, len);
    // 有差别
    KBSET::Changed[KBSET::Current] = true;
    memcpy(BufferOld[KBSET::Current], buf, len);
    Serial.print("VID:"); Serial.print(KBSET::VID[KBSET::Current],HEX);
    Serial.print(" PID:"); Serial.println(KBSET::PID[KBSET::Current],HEX);
    for (uint8_t i = 0; i < KBSET::BufferSize[KBSET::Current]; i++) {
      Serial.print((byte)(KBSET::Buffer[KBSET::Current][i]), HEX);
      Serial.print(" ");
    }
    Serial.println("");
  }
}

工作流程:当有按键事件发生时,库会调用 ParseHIDData() 这个函数,从设备获得的设备数据放在buf,产生数据的设备PID和 VID 在HIDUniversal::PID和HIDUniversal::VID中。之后,我们会在KBSET::PID VID数组中查找当前产生数据的设备是否已存在,如果没有话,将这个设备的PID和VID 加入数组中。之后再将改设备产生的数据存放到Buffer 中。之后,再判断新取得的数据和之前的BufferOld中保存的是否相同,如果相同就打印输出。

这样,我们就能分别获得三个设备产生的按键信息。

我是用了一个大键盘,一个小键盘和一个barcode scanner 作为测试设备,可以看到串口能够输出。

工作测试的视频:

https://www.bilibili.com/video/BV1nu411q7i7?share_source=copy_web

完整代码下载:

使用 MAX3421e 一个潜在的风险是:芯片本身价格高,比如前一段这个芯片价格炒到50以上,并且有假货。

手工分析一个 GPT 分区的U盘

最近在研究FAT32 格式,研究的目标是一个256MB容量的虚拟硬盘。

最开始发现资料和手上的结果多有出入无法对的上。经过两天的研究恍然大悟:对不上是因为这个硬盘在FAT前面还有分区信息,就是说对于一个硬盘来说要先读取分区的划分信息,比如第一个分区是FAT32,第二个分区是NTFS……..之后再根据不同的分区类型解析出文件内容。

目前主流分区有2种:一种是传统的 MBR;另外一种是比较新的 GPT。这次实验的硬盘是GPT 分区。

硬盘整体数据分布如下,解析是从左到右的顺序:

布局分布【参考1】

为了方便查看数据,使用 WinHex 这个磁盘工具。

1.使用WinHex进行查看,打开硬盘:

2.可以看到这个U盘上有“Start Sectors” 、“Partition 1(e:)”和“Partition gap”三块内容。对于OS来说 Partition 1 就是展现给用户的 e盘。

3.具体分析

3.1 Start Sectors 对应着LBA 0-33,使用WinHex 自带模板分析

分析结果如下:

这是一个保护性的“假MBR”,作用是防止不识别GPT分区的格式意外破坏分区。0x0-0x1FF偏移(在LBA0中):提供了一个 Partition 的数组, EDK2示例代码会检查这里确定是“假MBR”;0x200-0x258 偏移(在LBA1中):给出第一个分区信息表的入口(Partition Entry LBA),一般为2,意思是这个信息表入口在 LBA 2 上;0x400-0x4A0偏移(在 LBA2中):给出第一个分区的入口(Starting LBA),这里可以看到第一个分区从 LBA 2048(D)开始

3.2 选中第一个分区,可以看到它位于 0x100000处(也就是 LBA 2048 处)。同样的继续使用内置模板进行查看获得基本信息

从上面我们可以看到,从这个位置开始 Reserved 了6190(D)个扇区,因此可以知道第一个FAT在LBA:2048+6190=8238(D)。查看 LBA 8238 (D)这个位置 ,看到的就是 FAT 表。下图框出来的就是每一个文件项。第一个簇标记为 0xFFFFFFF8,第二个簇为0xFFFFFFFFF(0号FAT项为肮脏标志;1号FAT项是一个结束标志,通常簇号是从2开始就是这个原因)。其余的0xFFFFFF0F表示这个簇对应的是目录或者文件的结尾。之前我们的讨论都是以扇区为单位的,等到了研究文件时,通常需要以簇为单位,二者之间的换算关系是 Sector Per cluster 参数给出的。例如,这个值为4,就表示1个Cluster 由4个Sector 组成。

3.3 上面的概念不太容易理解,直接跳到数据区来进行查看。数据区的开始在:2048+6190+1001+1001=10240 扇区的位置:

可以隐约看到右侧ASCII字符就是我们FAT32分区上的根目录(下图是切换到D: 的示意图,注意物理硬盘和分区上的扇区不同,本文都是以物理硬盘的扇区号为准):

我们知道 EFI 目录下有 BOOT 目录,其中有 BOOTX64.EFI 这个文件。我们下面的目标就是获得这个文件的内容。首先,移动光标到前面的 EFI 目录字样处

从菜单上选择模板 :

查看目录信息 如下:

目录EFI 的数据在数据区起始扇区号+(簇号-2)*(扇区数/簇)

6号簇的绝对LBA就是 10240+(6-2)*4==10256扇区

上面可以看到 BOOT 这个目录,再使用模板解析BOOT目录的数据

可以看到 BOOT 目录的数据位于9号簇,对应的绝对扇区是10240+(9-2)*4==10268

再查看这个扇区

这里就能看到 BOOTX64.EFI 文件了,再用模板解析 BOOTX64.EFI 的数据:

最终查看10240+(10-2)*4==10272扇区,就是 BOOTX64.EFI 的内容

从 10272到10275 是10号簇,接下来需要到 FAT中查看,可以看到下面的位置指向了11号簇,就是说BOOTX64.EFI 文件并未完结,接下来的内容可以在11号簇所在扇区

参考:

  1. http://www.jinbuguo.com/storage/gpt.html  GPT 分区详解
  2. https://www.dgxue.com/huifu/luoji/windwos-huifu/fat32/
  3. https://blog.csdn.net/li_wen01/article/details/79929730