Step to UEFI (43) —– 取得当前目录的 EFI_DEVICE_PATH_PROTOCOL

在 EFI_SHELL_PROTOCOL 中提供了 EfiShellGetDevicePathFromFilePath 函数

具体可以在 \ShellPkg\Application\Shell\ShellProtocol.h 看到原型

/**
  Converts a file system style name to a device path.

  This function converts a file system style name to a device path, by replacing any
  mapping references to the associated device path.

  @param[in] Path               The pointer to the path.

  @return                       The pointer of the file path. The file path is callee
                                allocated and should be freed by the caller.
  @retval NULL                  The path could not be found.
  @retval NULL                  There was not enough available memory.
**/
EFI_DEVICE_PATH_PROTOCOL *
EFIAPI
EfiShellGetDevicePathFromFilePath(
  IN CONST CHAR16 *Path
  )

 

在【参考1】中,可以看到下面的介绍

getcurdp

经过试验,这个函数结合之前我们获得当前目录的函数完全可以达到目标。但是,如同【参考2】提到的,某些情况下,因为 Shell 不提供 EFI_SHELL_PROTOCOL,这个函数无法使用。

再查查 EFI_SHELL_ENVIRONMENT2 发现其中有功能类似的 NameToPath 函数

Convert a file system style name to a device path.

This function will convert a shell path name to a Device Path Protocol path. This function will allocate any required memory for this operation and it is the responsibility of the caller to free that memory when no longer required.

If anything prevents the complete conversion free any allocated memory and return NULL.

Parameters
[in]	Path	The path to convert.
Return values
!NULL	A pointer to the callee allocated Device Path.
NULL	The operation could not be completed.

 

最后,决定仿照 ShellLib.h 中的 ShellGetCurrentDir,写一个混合上述两种函数的函数。最后的代码就是这样

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

#include  <stdio.h>
#include  <stdlib.h>
#include  <wchar.h>

#include <Protocol/EfiShell.h>
#include <Library/ShellLib.h>

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

extern EFI_SHELL_PROTOCOL            *gEfiShellProtocol;
extern EFI_SHELL_ENVIRONMENT2 		 *mEfiShellEnvironment2;
/**
  GET  DEVICEPATH
**/
EFI_DEVICE_PATH_PROTOCOL *
EFIAPI
ShellGetDevicePath (
  IN CHAR16                     * CONST DeviceName OPTIONAL
  )
{
  //
  // Check for UEFI Shell 2.0 protocols
  //
  if (gEfiShellProtocol != NULL) {
    return (gEfiShellProtocol->GetDevicePathFromFilePath(DeviceName));
  }

  //
  // Check for EFI shell
  //
  if (mEfiShellEnvironment2 != NULL) {
    return (mEfiShellEnvironment2->NameToPath(DeviceName));
  }

  return (NULL);
}

int
EFIAPI
main (
  IN int Argc,
  IN char **Argv
  )
{
  EFI_DEVICE_PATH_PROTOCOL *DevicePath;
  CHAR16 *R;
  
  R=(CHAR16 *)ShellGetCurrentDir(NULL);
  
  Print(L"Current dir [%s]\n",ShellGetCurrentDir(0));
  
  DevicePath=ShellGetDevicePath(R);
  
  Print(L"Type        [%d]\nSub-Type    [%d]\nLength      [%d]\n",
				DevicePath->Type,
				DevicePath->SubType,
				DevicePath->Length[0] + DevicePath->Length[1]*256);
  
  return EFI_SUCCESS;
}

 

运行结果

getCurDP

代码下载

GetCurDP

参考:

1.UEFI_Shell_Spec_2_1_July02release P28
2.http://www.lab-z.com/getcurdir2/ Shell GetCurDir 补遗
3.http://www.bluestop.org/edk2/docs/UDK2010.SR1/struct_e_f_i___s_h_e_l_l___e_n_v_i_r_o_n_m_e_n_t2.html EFI_SHELL_ENVIRONMENT2 Struct Reference

Step to UEFI (37) —– SetTimer 设定定时器(上)

众所周知:UEFI中没有中断(UEFI唯一一个中断int 0,timer )【参考1】,如果想实现一个定时器的功能,必须使用 Event。

实现的思路是:

1. CreateEvent 创建 Timer Event
2. SetTimer 设定 Periodic 触发
3. SetTimer 关闭定时器
4. CloseEvent 销毁 Timer Event

首先研究 CreateEvent ,这个函数是Boot Service中提供的【参考3】

settimer1

第一个参数给出创建的类型,我们要选择EVT_TIMER;第二个参数是优先级,对我们来说影响不大;第三个参数给出当Event发生时对应的处理函数;第四个参数我的理解是自定义的数据;第五个参数是创建出来的Event。

接下来再看看SetTimer函数,同样也是 Boot Service 中提供的服务

settimer2

第一个参数是你创建的Event;然后是Timer的类型,比如:周期性触发;最后是设定Timer的时间,多久触发一次,单位是100ns。

CloseEvent就很简单了

settimer3

程序还参考了 ShellPkg\Library\UefiShellNetwork1CommandsLib\Ping.c 的代码。

最终代码如下

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

#include  <stdio.h>
#include  <stdlib.h>
#include  <wchar.h>

#include <Protocol/EfiShell.h>
#include <Library/ShellLib.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>

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

extern EFI_SHELL_ENVIRONMENT2    *mEfiShellEnvironment2;
extern EFI_HANDLE				 gImageHandle;

STATIC CONST UINTN SecondsToNanoSeconds = 10000000;

UINTN	Counter = 0;
/**
  The callback function for the timer event used to get map.

  @param[in] Event    The event this function is registered to.
  @param[in] Context  The context registered to the event.
**/
VOID
EFIAPI
Timeout (
  IN EFI_EVENT      Event,
  IN VOID           *Context
  )
{
  Print(L"www.lab-z.com [%d]\r\n",++ Counter);
  return ;
}

int
EFIAPI
main (                                         
  IN int Argc,
  IN char **Argv
  )
{
  EFI_STATUS                Status;
  EFI_HANDLE                TimerOne = NULL;
  BOOLEAN					ExitMark=FALSE;
  
  Status  = gBS->CreateEvent (
                    EVT_NOTIFY_SIGNAL | EVT_TIMER,
                    TPL_CALLBACK,
                    Timeout,
                    NULL,
                    &TimerOne
                    );
    
    if (EFI_ERROR (Status)) {
        Print(L"Create Event Error! \r\n");
		return ;
    }

    Status = gBS->SetTimer (
                   TimerOne,
                   TimerPeriodic,
                   MultU64x32 (SecondsToNanoSeconds, 1)
                   );
    
    if (EFI_ERROR (Status)) {
        Print(L"Set Timer Error! \r\n");
		return ;
    }

	while (!ExitMark)
	{
		if (mEfiShellEnvironment2 -> GetExecutionBreak()) {ExitMark=TRUE;}
	}
    gBS->SetTimer (TimerOne, TimerCancel, 0);
    gBS->CloseEvent (TimerOne);	

  return EFI_SUCCESS;
}

 

运行结果如下
TimerTest

完整代码下载
TimerTest

后记:这部分对我来说还是比较复杂,在描述上定义概念可能会有偏差,如果阅读中发现,欢迎通知我及时改正。

参考:

1. http://blog.csdn.net/celiaqianhj/article/details/7180783 UEFI Events
2. http://biosren.com/viewthread.php?tid=2095&highlight=%B6%A8%CA%B1 什么是EFI Events?
3. UEFI Spec 2.4 P118

Step to UEFI (35) —– How to build Shell.efi

【特别提醒:下面的全部操作都是在UDK2014中完成,具体代码会与2010有差别】

第一个问题:我们运行的模拟环境(NT32)中的Shell是来自哪里?

回答:在 \Nt32Pkg\Nt32Pkg.fdf 中你可以看到下面的定义

################################################################################
#
# FILE statements are provided so that a platform integrator can include
# complete EFI FFS files, as well as a method for constructing FFS files
# using curly "{}" brace scoping. The following three FILEs are
# for binary shell, binary fat and logo module.
#
################################################################################
INF EdkShellBinPkg/FullShell/FullShell.inf

INF FatBinPkg/EnhancedFatDxe/Fat.inf

FILE FREEFORM = PCD(gEfiIntelFrameworkModulePkgTokenSpaceGuid.PcdLogoFile) {
    SECTION RAW = MdeModulePkg/Logo/Logo.bmp

如果你把上面的 FullShell.inf 替换成 EdkShellBinPkg\MinimumShell 下面的MinimumShell.inf 再次编译之后会发现使用的是Mini版本的Shell. 例如: Hexedit 这个命令只在Full版本中才有,Mini版本下不支持。(实验时候特别注意,如果你 Build了 MinimumShell.inf, 在 fsnt1: 下面有一个 Hexedit.efi)

2.如何重新Build Shell.efi?

根据 ShellBinPkg 目录下的 Readme.txt (没错,我也不知道为什么是在这个目录而不是ShellPkg下面的 ReadMe.txt) ,可以使用下面的命令进行Build:

build -a IA32 -p ShellPkg\ShellPkg.dsc -b RELEASE

3.实际操作。这是我们最常见到的Shell的模样,我下面要尝试给他添加一段String.

full

在 \ShellPkg\Application\Shell\Shell.c 可以看到输出版本信息的语句

    //
    // Display the version
    //
    if (!ShellInfoObject.ShellInitSettings.BitUnion.Bits.NoVersion) {
      ShellPrintHiiEx (
        0,
        gST->ConOut->Mode->CursorRow,
        NULL,
        STRING_TOKEN (STR_VER_OUTPUT_MAIN_SHELL),
        ShellInfoObject.HiiHandle,
        SupportLevel[PcdGet8(PcdShellSupportLevel)],
        gEfiShellProtocol->MajorVersion,
        gEfiShellProtocol->MinorVersion
       );

      ShellPrintHiiEx (
        -1,
        -1,
        NULL,
        STRING_TOKEN (STR_VER_OUTPUT_MAIN_SUPPLIER),
        ShellInfoObject.HiiHandle,
        (CHAR16 *) PcdGetPtr (PcdShellSupplier)
       );

      ShellPrintHiiEx (
        -1,
        -1,
        NULL,
        STRING_TOKEN (STR_VER_OUTPUT_MAIN_UEFI),
        ShellInfoObject.HiiHandle,
        (gST->Hdr.Revision&0xffff0000)>>16,
        (gST->Hdr.Revision&0x0000ffff),
        gST->FirmwareVendor,
        gST->FirmwareRevision
       );
    }

    //
    // Display the version
    //
    if (!ShellInfoObject.ShellInitSettings.BitUnion.Bits.NoVersion) {
      ShellPrintHiiEx (
        0,
        gST->ConOut->Mode->CursorRow,
        NULL,
        STRING_TOKEN (STR_VER_OUTPUT_MAIN_SHELL),
        ShellInfoObject.HiiHandle,
        SupportLevel[PcdGet8(PcdShellSupportLevel)],
        gEfiShellProtocol->MajorVersion,
        gEfiShellProtocol->MinorVersion
       );

      ShellPrintHiiEx (
        -1,
        -1,
        NULL,
        STRING_TOKEN (STR_VER_OUTPUT_MAIN_SUPPLIER),
        ShellInfoObject.HiiHandle,
        (CHAR16 *) PcdGetPtr (PcdShellSupplier)
       );

      ShellPrintHiiEx (
        -1,
        -1,
        NULL,
        STRING_TOKEN (STR_VER_OUTPUT_MAIN_UEFI),
        ShellInfoObject.HiiHandle,
        (gST->Hdr.Revision&0xffff0000)>>16,
        (gST->Hdr.Revision&0x0000ffff),
        gST->FirmwareVendor,
        gST->FirmwareRevision
       );
    }

	//LabZDebug_Start 加入我们定义的String
	ShellPrintHiiEx (
        -1,
        -1,
        NULL,
        STRING_TOKEN (STR_LABZ_UEFI),
        ShellInfoObject.HiiHandle
       );
	//LabZDebug_End

    //
    // Display the mapping
    //

    if (PcdGet8(PcdShellSupportLevel) >= 2 && !ShellInfoObject.ShellInitSettings.BitUnion.Bits.NoMap) {
      Status = RunCommand(L"map", NULL);
      ASSERT_EFI_ERROR(Status);
    }

同时在 \ShellPkg\Application\Shell\Shell.uni 加入我们自定义的字符串

#string STR_LABZ_UEFI		          #language en-US "www.lab-z.com 2014/12/12 build.....\r\n"

最后的结果如下,可以看到多出来一行我们自己定义的字符串:

modify

特别提醒:请注意文章中两张图片,实际上盘符是有差别的,一个是 FSNTx: 一个是 FSx。就是说在虚拟环境下模拟出来的盘符还是有差别的。目前我不清楚这个差别是如何导致的。

========================================================================
2015年3月31日补充

1.build shell.efi 之后生成的文件在 Build\Shell\RELEASE_MYTOOLS\IA32下面。同样他的子目录中也能找到Shell.efi (总共三个),他们内容相同

2.有一种情况是你替换了 Shell_Full.efi 之后,模拟器无法进入 shell,始终停留在Setup中。这种情况请检查输出的Log信息,我遇到的情况是因为改动有问题,导致 Shell.efi 是损坏的,无法正常Load起来。

Step to UEFI (32) —– GetFileInfo

这里介绍一个能够获得文件基本信息的函数: GetFileInfo

可以在 \ShellPkg\Include\Library\ShellLib.h 看到定义

/**
  This function will retrieve the information about the file for the handle
  specified and store it in allocated pool memory.

  This function allocates a buffer to store the file's information. It is the
  caller's responsibility to free the buffer.

  @param[in] FileHandle         The file handle of the file for which information is
                                being requested.

  @retval NULL                  Information could not be retrieved.

  @return                       The information about the file.
**/
EFI_FILE_INFO*
EFIAPI
ShellGetFileInfo (
  IN SHELL_FILE_HANDLE          FileHandle
  );

 

需要FileHandle作为输入函数,输出结果是 EFI_FILE_INFO 结构体。 这个结构体可以在
\MdePkg\Include\Guid\FileInfo.h 这个文件中看到(同时在 \EdkCompatibilityPkg\Foundation\Efi\Protocol\FileInfo\FileInfo.h 里面也有一个定义,只是这个定义未参加编译)。需要注意,调用的函数负责给结果分配一块内存,你自己的程序要负责释放这块内存的。

typedef struct {
  ///
  /// The size of the EFI_FILE_INFO structure, including the Null-terminated FileName string.
  ///
  UINT64    Size;
  ///
  /// The size of the file in bytes.
  ///
  UINT64    FileSize;
  ///
  /// PhysicalSize The amount of physical space the file consumes on the file system volume.
  ///
  UINT64    PhysicalSize;
  ///
  /// The time the file was created.
  ///
  EFI_TIME  CreateTime;
  ///
  /// The time when the file was last accessed.
  ///
  EFI_TIME  LastAccessTime;
  ///
  /// The time when the file's contents were last modified.
  ///
  EFI_TIME  ModificationTime;
  ///
  /// The attribute bits for the file.
  ///
  UINT64    Attribute;
  ///
  /// The Null-terminated name of the file.
  ///
  CHAR16    FileName[1];
} EFI_FILE_INFO;

 

看这个结构体可以得知,我们能够获得文件的大小,创建时间,修改时间属性文件名等等。

对于时间的定义 EFI_TIME 可以在 \BaseTools\Source\C\Include\Common\UefiBaseTypes.h 看到。相比之前我们看过的 time_t ,这个结构体是很单纯的定义,不需要换算:

// EFI Time Abstraction:
//  Year:       2000 - 20XX
//  Month:      1 - 12
//  Day:        1 - 31
//  Hour:       0 - 23
//  Minute:     0 - 59
//  Second:     0 - 59
//  Nanosecond: 0 - 999,999,999
//  TimeZone:   -1440 to 1440 or 2047
//
typedef struct {
  UINT16  Year;
  UINT8   Month;
  UINT8   Day;
  UINT8   Hour;
  UINT8   Minute;
  UINT8   Second;
  UINT8   Pad1;
  UINT32  Nanosecond;
  INT16   TimeZone;
  UINT8   Daylight;
  UINT8   Pad2;
} EFI_TIME;

 

对于 Attribute 的定义,在 \MdePkg\Include\Protocol\SimpleFileSystem.h

//
// File attributes
//
#define EFI_FILE_READ_ONLY  0x0000000000000001ULL
#define EFI_FILE_HIDDEN     0x0000000000000002ULL
#define EFI_FILE_SYSTEM     0x0000000000000004ULL
#define EFI_FILE_RESERVED   0x0000000000000008ULL
#define EFI_FILE_DIRECTORY  0x0000000000000010ULL
#define EFI_FILE_ARCHIVE    0x0000000000000020ULL
#define EFI_FILE_VALID_ATTR 0x0000000000000037ULL

 

最后,写一个程序验证一下

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

#include  <stdio.h>
#include  <stdlib.h>
#include  <wchar.h>

#include <Protocol/EfiShell.h>
#include <Library/ShellLib.h>

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

extern EFI_SHELL_PROTOCOL        *gEfiShellProtocol;

int
EFIAPI
main (
  IN int Argc,
  IN char **Argv
  )
{
  EFI_FILE_HANDLE   FileHandle;
  RETURN_STATUS     Status;
  EFI_FILE_INFO     *FileInfo = NULL;
  
  Status = ShellOpenFileByName(L"fsnt0:", (SHELL_FILE_HANDLE *)&FileHandle,
                               EFI_FILE_MODE_READ , 0);

  if(Status != RETURN_SUCCESS) {
        Print(L"OpenFile failed!\n");
		return EFI_SUCCESS;
      }							   

  FileInfo = ShellGetFileInfo( (SHELL_FILE_HANDLE)FileHandle);	

  Print(L"Filesize [%ld] bytes\n",FileInfo-> FileSize);
  Print(L"PhysicalSize [%ld] bytes\n",FileInfo-> PhysicalSize);  

  Print(L"File Create date [%d-%d-%d %d-%d-%d]\n",
		FileInfo-> CreateTime.Year,
		FileInfo-> CreateTime.Month,
		FileInfo-> CreateTime.Day,
		FileInfo-> CreateTime.Hour,
		FileInfo-> CreateTime.Minute,
		FileInfo-> CreateTime.Second);    
		
  Print(L"File last accessed date [%d-%d-%d %d-%d-%d]\n",
		FileInfo-> LastAccessTime.Year,
		FileInfo-> LastAccessTime.Month,
		FileInfo-> LastAccessTime.Day,
		FileInfo-> LastAccessTime.Hour,
		FileInfo-> LastAccessTime.Minute,
		FileInfo-> LastAccessTime.Second);   		
		
  Print(L"File last modification date [%d-%d-%d %d-%d-%d]\n",
		FileInfo-> ModificationTime.Year,
		FileInfo-> ModificationTime.Month,
		FileInfo-> ModificationTime.Day,
		FileInfo-> ModificationTime.Hour,
		FileInfo-> ModificationTime.Minute,
		FileInfo-> ModificationTime.Second);   		

  Print(L"File Name [%s]\n",&FileInfo->FileName[0]);  
		
  free(FileInfo);  // Free up the buffer from ShellGetFileInfo()  
  
  return EFI_SUCCESS;
}

 

getfileinfo

代码下载:
GetFileInfo

参考:

1.本文的例子参考 \ShellPkg\Library\UefiShellLevel3CommandsLib\Touch.c

USB 控制七段数码管(II)

换个免驱的方式。前面之所以能免驱动,是因为我走的是USB HID协议。除了这个,更常见的是USB Mass Storage协议,就是我们常见的U盘走的协议。对于这个协议在《圈圈教你玩USB》中有详细介绍。下面就讲一下如何把我这个设备移植到MSD协议上。

首先说一下Firmware设计上的改变。

驱动数码管同样使用之前HID中的那种,用一个Timer不断循环点亮每一个数码管。因此,在Main.c中加入Timer0的初始化和中断服务程序。它会按照zBuf中给出来的依次点亮四个数码管。

volatile uint8 zBuf[]={1,2,3,4}; //用来保存8字节的输出报告。

/********************************************************************
函数功能:定时器0初始化,用来做键盘扫描。
入口参数:无。
返    回:无。
备    注:无。
********************************************************************/
void InitTimer0(void)
{
 TMOD&=0xF0;
 TMOD|=0x01;
 ET0=1;
 TR0=1;
}

/********************************************************************
函数功能:定时器0中断处理。
入口参数:无。
返    回:无。
备    注:22.1184M晶体约5ms中断一次。
********************************************************************/
void Timer0Isr(void) interrupt 1
{ 
   static i=0; 

//定时器0重装,定时间隔为5ms,加15是为了修正重装所花费时间
//这个值可以通过软件仿真来确定,在这里设置断点,调整使两次运行
//时间差刚好为5ms即可。
 TH0=(65536-Fclk/1000/12*5+15)/256;	 
 TL0=(65536-Fclk/1000/12*5+15)%256;   // 

	P2=1 << i;
	P1=zBuf[i];
	i++;
	if (i==4) {i=0;}
}
/*******************************************************************/

 

PC对USB Mass Storage 设备传数据,使用的是 WRITE(10) 命令,具体的数据会在批量传输协议中的批量数据阶段传输给设备。具体对应在代码中 SCSI.C 中的 procScsiOutData 中,我们在这里将收到的输局赋值给 zBuf.

/********************************************************************
函数功能:处理输出数据。
入口参数:无。
返    回:无。
备    注:无。
********************************************************************/
void ProcScsiOutData(void)
{
 uint8 Len;
 int i;

 //读端点2数据
 Len=D12ReadEndpointBuffer(4,EP2_SIZE,Ep2Buffer);

 //LabzDebug_Start 
 zBuf[0]=Ep2Buffer[0]; zBuf[1]=Ep2Buffer[1]; zBuf[2]=Ep2Buffer[2]; zBuf[3]=Ep2Buffer[3];

  PrintHex(zBuf[0]);
  PrintHex(zBuf[1]);
  PrintHex(zBuf[2]);
  PrintHex(zBuf[3]);
  Prints("\r\n");

 #ifdef DEBUG1
 Prints("收到 \r\n");
 for ( i=0; i <4;i++)
 {
  PrintHex(*(Ep2Buffer+i));  //如果需要显示调试信息,则显示发送的数据
  if(((i+1)%16)==0)Prints("\r\n"); //每16字节换行一次
  PrintHex(zBuf[0]);
  PrintHex(zBuf[1]);
  PrintHex(zBuf[2]);
  PrintHex(zBuf[3]);
  if((Len%16)!=0)Prints("\r\n"); //换行
 }
 #endif
 //LabZDebug_End
 Ep2DataLength-=Len;
 //清除端点缓冲区
 D12ClearBuffer();
 //由于没有存储器,这里将缓冲区清0模拟数据处理
 while(Len)
 {
  Ep2Buffer[Len]=0; //缓冲区清0
  Len--;
 }
 
 //数据传输完毕,进入到状态阶段
 if(Ep2DataLength==0)
 {
  //此时Ep2DataLength为0,并且处于数据阶段,调用发送数据函数将返回CSW
  Ep2SendData();
 }
}

 

只要做这一点Change,Firmware即可支持。

其次说一下上位机程序的设计。

因为模拟成一个U盘,因此,按照访问硬盘的方式即可传输数据。先是使用CreateFile打开PhysicalDriverX (特别注意:从PhysicalDriver0开始),之后就用 WriteFile 进行写入,特别注意,写入的最小单位是512字节,不能小于这个值。最后CloseHandle关闭即可。

最初,程序设计出来是这样的

CreateFile(PhysicalDrive3)

while Keypressed=false
{
WriteFile (512 Byte)
}

CloseHandle

结果发现非常奇怪,只有第一次能够顺利写入512Bytes,之后会不断出现 Error 5 ERROR_ACCESS_DENIED 5 (0x5) Access is denied. 的问题。为了防止是我设备不稳定造成的,我还用一个U盘进行试验(特别注意,操作会导致U盘内容不可读!!!)在Windows 7 x64和WindowsXP 32 都有同样的现象。请教天杀,他回复“想起来了,Win7的确有这个问题,可以写分区表,但是不能写后续的分区位置,是会被拒绝的。要写数据必须通过分区去写,或者将分区删除。你可以把设备识别成USB HDD,然后没有分区,这样应该可以全盘去写。如果是没有分区的那种U盘形式,是可能会被禁止写入的。”

这样的话,第一个方案就出来了,每次都要用CreateFile打开硬盘,写入之后再Close。

#include "stdafx.h"
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <time.h>

int main(int argc, char *argv[])
{
   HANDLE hFile;
   BOOL fSuccess;
   char txchar[512];
   DWORD iBytesWritten;
   byte Seven_Dig[10]={   0x40,  // = 0
						  0x79,  // = 1	
						  0x24,  // = 2
						  0x30,  // = 3
                          0x19,  // = 4	
						  0x12,  // = 5
						  0x02,  // = 6
                          0x78,  // = 7	
						  0x00,  // = 8
						  0x10,  // = 9
                             }; 



 int n=1234;	
 while (kbhit()==0)
  {
	  hFile = CreateFile(L"\\\\.\\PhysicalDrive2",
                    GENERIC_WRITE,
                    FILE_SHARE_WRITE,    // must be opened with exclusive-access
                    NULL, // no security attributes
                    OPEN_EXISTING, // must use OPEN_EXISTING
                    0,    // not overlapped I/O
                    NULL  // hTemplate must be NULL for comm devices
                    );

   if (hFile == INVALID_HANDLE_VALUE) 
   {
       // Handle the error.
        printf ("CreateFile failed with error %d.\n", GetLastError());
		system("PAUSE");
        return (1);
   }

   txchar[0]=Seven_Dig[n / 1000 % 10];
   txchar[1]=Seven_Dig[n / 100 % 10];
   txchar[2]=Seven_Dig[n / 10 % 10];
   txchar[3]=Seven_Dig[n %10];
								
  for (int i=1;i<512 /4;i++) 
   {
 txchar[i*4]=txchar[0];
 txchar[i*4+1]=txchar[1];
  txchar[i*4+2]=txchar[2];
  txchar[i*4+3]=txchar[3];
   }

fSuccess=WriteFile(hFile, &txchar, 512, &iBytesWritten,NULL);
printf ("done send %d bytes.Status [%d] \n", iBytesWritten,fSuccess);
printf ("Get lasterror %d\n", GetLastError());
printf("%d \n",n);

n=(n++)%10000;
FlushFileBuffers(hFile);
Sleep(1000);
 CloseHandle(hFile);
}//while (kbhit()==0)

   system("PAUSE");


   return (0);
}

 

第二个方案就是每次用WriteFile写入之后,我们再重置指针,再从最开始写入。类似下面的方案。

CreateFile(PhysicalDrive3)

while Keypressed=false
{
SetFilePointer(hFile,0,0,FILE_BEGIN);
WriteFile (512 Byte)
}

CloseHandle

实际的效果和之前的HID的方式相同。

Firmware下载 USB7SegFW

第一种上位机程序下载
exp1

第二种上位机程序下载
exp2

通过WriteFile对硬盘发送数据

下面这个程序首先用 CreateFile 打开 PhysicalDiskX 然后使用 CreateFile 向里面写入数据。

#include "stdafx.h"
#include <windows.h>
#include <stdio.h>
#include <conio.h>

int main(int argc, char *argv[])
{
   HANDLE hFile;
   BOOL fSuccess;
   char txchar[512];
   DWORD iBytesWritten;

   hFile = CreateFile(L"\\\\.\\PhysicalDrive1",
                    GENERIC_WRITE,
                    FILE_SHARE_WRITE,    // must be opened with exclusive-access
                    NULL, // no security attributes
                    OPEN_EXISTING, // must use OPEN_EXISTING
                    0,    // not overlapped I/O
                    NULL  // hTemplate must be NULL for comm devices
                    );

   if (hFile == INVALID_HANDLE_VALUE) 
   {
       // Handle the error.
        printf ("CreateFile failed with error %d.\n", GetLastError());
		system("PAUSE");
        return (1);
   }

   txchar[0]='l'; txchar[1]='a';txchar[2]='b';txchar[3]='z';

   //有资料说用这个函数对磁盘写的最小值是 512 bytes 我实验了一下如果小于此会出现error
   fSuccess=WriteFile(hFile, &txchar, 512, &iBytesWritten,NULL);
   printf ("done send %d bytes.Status [%d] \n", iBytesWritten,fSuccess,iBytesWritten);
   printf ("Get lasterror %d\n", GetLastError());

   system("PAUSE");
   CloseHandle(hFile);

   return (0);
}

 

我的实验是在虚拟机中进行的。特别提醒:本次实验可能会损害你的数据,请务必小心。如果系统有安装杀毒软件,有可能被判定为病毒行为。实验的目标盘是一个U盘,盘符为 G:

a

之后,先用 Winhex 打开,我们可以看到开始的几个字符是 abcd

b

运行我们的程序(可以看到发送 512 Bytes Status=1 表示 Success GetLastError=0 表示无错误)
c

我们需要重新打开一次硬盘(WinHex有磁盘修改感知功能,但是在这里不好用)
e

我们可以看到最开始的几个字符被修改了,这说明我们的程序是由效果的的。
d

完整的程序下载:

sendbywritefile

参考:

1.http://msdn.microsoft.com/en-us/library/windows/desktop/aa365747(v=vs.85).aspx MSDN WriteFile function

2.http://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx MSDN System Error 对于确定 GetLastError 返回值很有用

3.http://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx MSDN CreateFile function

Step to UEFI (18) —– CLib 获得 ImageHandle

前面介绍了,用CLib我们可以编写出普通C语言一样的代码。入口是 main (int Argc, char **Argv),但是如何获得当前的 ImageHandle 呢?【参考1】给出了一个答案。查看实际的入口ShellCEntryLib (ShellPkg\Library\UefiShellCEntryLib\UefiShellCEntryLib.c)

EFI_STATUS
EFIAPI
ShellCEntryLib (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  INTN                           ReturnFromMain;
  EFI_SHELL_PARAMETERS_PROTOCOL *EfiShellParametersProtocol;
  EFI_SHELL_INTERFACE           *EfiShellInterface;
  EFI_STATUS                    Status;

 

就是说 Shell Call进来的时候实际上和普通的EFI 程序没有差别,都是给了ImageHandle的,只是后面调用的时候去掉了。调用的顺序是 ShellCEntryLib –> ShellAppMain –> main.c(自己写的),所以如果我们能够加入一个参数,那么可以直接把ImageHandle传进去。
那就手动加入这个参数吧,需要修改的文件和内容如下:
1. ShellPkg\Library\UefiShellCEntryLib\UefiShellCEntryLib.c

if (!EFI_ERROR(Status)) {
    //
    // use shell 2.0 interface
    //
    ReturnFromMain = ShellAppMain (
					   ImageHandle,  //

 

2. ShellPkg\Inlcude\Library\ShellCEntryLib.h

INTN
EFIAPI
ShellAppMain (
  IN EFI_HANDLE        ImageHandle, //

 

3. StdLib\LibC\Main\Main.c

INTN
EFIAPI
ShellAppMain (
IN EFI_HANDLE ImageHandle, //<——-Added
IN UINTN Argc,
IN CHAR16 **Argv
)

4.我们自己的程序 main.c ,和之前文章提到的只有main被修改了

/** @file
    A simple, basic, application showing how the Hello application could be
    built using the "Standard C Libraries" from StdLib.

    Copyright (c) 2010 - 2011, Intel Corporation. All rights reserved.<BR>
    This program and the accompanying materials
    are licensed and made available under the terms and conditions of the BSD License
    which accompanies this distribution. The full text of the license may be found at
    http://opensource.org/licenses/bsd-license.

    THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS,
    WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.
**/
#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>

#include  <stdio.h>
#include  <stdlib.h>
#include <wchar.h>

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

//SimpleTextInputEx.harderr
//#define	EFI_SHIFT_STATE_VALID		0x80000000
//#define EFI_LEFT_CONTROL_PRESSED	0x00000002

EFI_STATUS
EFIAPI
NotificationFunction(
	IN EFI_KEY_DATA	*KeyData
)
{
	printf("This is a test from www.lab-z.com \n");		
	return(EFI_SUCCESS);
}

/***
  Demonstrates basic workings of the main() function by displaying a
  welcoming message.

  Note that the UEFI command line is composed of 16-bit UCS2 wide characters.
  The easiest way to access the command line parameters is to cast Argv as:
      wchar_t **wArgv = (wchar_t **)Argv;

  @retval  0         The application exited normally.
  @retval  Other     An error occurred.
***/
int
EFIAPI
main (
  IN EFI_HANDLE        ImageHandle,  //

 

之后还是使用 build –a IA32 –p AppPkg\AppPkg.dsc 来编译,之前如果里面还有编译器他的Application,那么需要从 AppPkg.dsc中先去掉其余的Application。
编译之,通过;运行之和之前的结果一样,好用。

buildimge

本文提到的程序可以在这里下载
Main

上面的程序看起来有些复杂,还会破坏自己的编译环境,后面我会继续研究找到更好的解决方法。就是这样。

参考:
1. 论坛上有人提出了类似的问题
http://biosren.com/viewthread.php?tid=4651&highlight=ShellCentrylib
一直有這個疑問~
目前寫的Shell APP的進入點,參數都是argc,argv
那要怎麼得到他的ImageHandle呢?

UefiShellCEntryLib.c
EFI_STATUS
EFIAPI
ShellCEntryLib (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)

MyShellApp.c
INTN
EFIAPI
ShellAppMain (
IN UINTN Argc,
IN CHAR16 **Argv
)

VS2008 等待按键

通常我们在 Console 的程序中需要等待按键之类的,通常使用的都是:conio.h中的getch() 或者 stdio.h中的getchar()【参考1】。美中不足的是,这两个函数都会一直在那里等待按键,不会去做其他的事情。如果我们需要实现类似Pascal语言中 Keypress 函数,“检查一下是否有按键,没有的话我继续做其他事情”的功能,就需要其他函数了。

那就是 conio.h中的kbhit()

#include "stdafx.h"
#include<conio.h>
#include<stdio.h>
#include "windows.h"

using namespace System;

int main(array<System::String ^> ^args)
{
  int i=0;
  while (!kbhit())
  {
	  printf("lab-z.com [%d]\n",i++);
	  Sleep(200);
  }
  system("PAUSE");
  return 0;
}

 

运行结果

keypressed
参考:

1.http://baike.baidu.com/view/7942479.htm 阻塞函数

Step to UEFI (17) —– Application中注册一个快捷键

前面介绍过如何在 Shell 下实现暂停和中断运行。这里介绍如何实现在Application中注册一个“热键”,当按下这个键的时候去做另外的事情。

根据资料,需要使用EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL 的 RegisterKeyNotify 【参考1,2】

具体的参考代码在ShellProtocol.c

KeyData.KeyState.KeyToggleState = 0;
KeyData.Key.ScanCode = 0;
KeyData.KeyState.KeyShiftState = EFI_SHIFT_STATE_VALID|EFI_LEFT_CONTROL_PRESSED;
KeyData.Key.UnicodeChar = L’c’;

Status = SimpleEx->RegisterKeyNotify(
SimpleEx,
&KeyData,
NotificationFunction,
&ShellInfoObject.CtrlCNotifyHandle1);

查阅Spec【参考3】,有如下资料

reg2

reg1

简单的说,第一个参数是EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL 的实例,第二个参数是按键信息,第三个参数是当按键发生时会被CallBack的函数,第四个参数是注册的Handle(第四个我也不太理解,琢磨一下应该是用来记录这个注册动作的东西,有了这个东西后面可以用来取消这次的注册)。

写一个简单的程序进行测试,在一个循环中,当按下 ctrl+s 的时候,自动在屏幕上输出字符串:

/** @file
    A simple, basic, application showing how the Hello application could be
    built using the "Standard C Libraries" from StdLib.

    Copyright (c) 2010 - 2011, Intel Corporation. All rights reserved.<BR>
    This program and the accompanying materials
    are licensed and made available under the terms and conditions of the BSD License
    which accompanies this distribution. The full text of the license may be found at
    http://opensource.org/licenses/bsd-license.

    THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS,
    WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.
**/
#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>

#include  <stdio.h>
#include  <stdlib.h>
#include <wchar.h>

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

EFI_STATUS
EFIAPI
NotificationFunction(
	IN EFI_KEY_DATA	*KeyData
)
{
	printf("This is a test from www.lab-z.com \n");		
	return(EFI_SUCCESS);
}

/***
  Demonstrates basic workings of the main() function by displaying a
  welcoming message.

  Note that the UEFI command line is composed of 16-bit UCS2 wide characters.
  The easiest way to access the command line parameters is to cast Argv as:
      wchar_t **wArgv = (wchar_t **)Argv;

  @param[in]  Argc    Number of argument tokens pointed to by Argv.
  @param[in]  Argv    Array of Argc pointers to command line tokens.

  @retval  0         The application exited normally.
  @retval  Other     An error occurred.
***/
int
EFIAPI
main (
	IN EFI_HANDLE		ImageHandle,
	IN EFI_SYSTEM_TABLE *SystemTable
  )
{
	EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *SimpleEx;
	EFI_KEY_DATA	KeyData;
	EFI_STATUS		Status;
	EFI_HANDLE		CtrlCNotifyHandle = NULL;
	INTN			i;

 	Status = gBS -> OpenProtocol(
		gST -> ConsoleInHandle,
		&gEfiSimpleTextInputExProtocolGuid,
		(VOID**)&SimpleEx,
		ImageHandle,
		NULL,
		EFI_OPEN_PROTOCOL_GET_PROTOCOL);

	if (EFI_ERROR(Status)) {
		printf("OpenProtocol ERROR!!\n");
	}		

	KeyData.KeyState.KeyToggleState = 0;
	KeyData.Key.ScanCode			= 0;
	KeyData.KeyState.KeyShiftState 	= EFI_SHIFT_STATE_VALID | EFI_LEFT_CONTROL_PRESSED;
	KeyData.Key.UnicodeChar			= L's';

	Status = SimpleEx -> RegisterKeyNotify (
			SimpleEx,
			&KeyData,
			NotificationFunction,
			&CtrlCNotifyHandle);

	for (i=0;i<200;i++) 
	 {
		printf("Test\n");		
		gBS -> Stall (5000);
      }

	Status = SimpleEx -> UnregisterKeyNotify(SimpleEx, CtrlCNotifyHandle);

  return EFI_SUCCESS;
}

 

运行结果(模拟坏境中测试的)

regkey

代码下载

registerkey

参考:
1. http://biosren.com/viewthread.php?tid=6666&highlight=RegisterKeyNotify
如何在UEFI shell做一個類似key board hook的方式?
2. http://biosren.com/viewthread.php?tid=6579&highlight=RegisterKeyNotify
UEFI HotKey事件
3. UEFI Specification 2.4 P440

USB 控制七段数码管

这次的目标是要实现用 USB 口来控制七段数码管。

硬件上面选用《圈圈教你玩USB》配套的实验板。上面的单片机为STC89S52。实验板上P0接口用来和D12通讯,P1接口连接到一组开关按钮上,P2接口上有一组LED,LED另外一端通过10K电阻上拉。下面用色块对这些端口的用途做了简单标记:

usbs1

最开始的设想是数码管(用的还是上次出场过的那一款数码管,共阳极,【参考1】)阳极连接到P1(可以理解为SEG选择 Pin接正极),阴极连接到P2(数码段 Pin接负极)。实验发现数码管根本不亮,检查很多次后尝试去掉六个数码段的连接终于能够点亮一段数字。这才恍然大悟,因为共阳极上还有板载的LED所以会使得电压很奇怪,不足以推动全部。同时显示,所以需要更改方案。

做到这里,我忽然明白极客工坊大神弘毅提到的:“驱动数码管限流电阻肯定是必不可少的,限流电阻有两种接法,一种是在d1-d4阳极接,总共接4颗。这种接法好处是需求电阻比较少,但是会产生每一位上显示不同数字亮度会不一样,1最亮,8最暗。另外一种接法就是在其他8个引脚上接,这种接法亮度显示均匀,但是用电阻较多。” 因为电压相同,数字1只需要点亮两根LED,8需要点亮七根。

修改之后,阳极连接到P2,阴极连接到P1上。实验发现即使4个数字同时显示仍然是可以清晰辨认的。连接如下图所示:

usbsc

继续尝试在上位机程序中自动扫描点亮每个LED,前面一篇Arduino就是使用类似的方法点亮的。实做之后发现这样做会导致每个数码管亮灭之间的间隔太长,这样在人类看起来是逐个亮灭的而不是同时亮。猜测这是程序本身发送和USB传输Delay太长导致的。初步估计每个数字显示间隔至少在200ms以上。如果想做一个类似跑马灯的程序用这样的方案没问题,但是如果想持续稳定的显示数字这个方案不可行。

最后上位机程序直接发送显示的数值,由Firmware本身解析,同时利用单片机上的定时器(15ms一次),不断刷新每个显示位。开始时,Firmware中定时器产生中断后,ISR中将四位数值轮流显示一次,但是实验结果肉眼只能看到最后一个点亮的数值。估计这个可能是因为ISR中每个数码位显示过快,而他们点亮的时间远远小于不在ISR中的时间。

经过无数次的调试后,终于完成了。

resize

拍了一个视频,PC来控制七段数码管

http://www.tudou.com/programs/view/-D_w29Pwavc/?resourceId=414535982_06_02_99

对应的Firmware关键点在于中断服务函数

void Timer0Isr(void) interrupt 1
{ 
//LabzDebug_Start
   static i=0; 
//LabzDebug_End

//定时器0重装,定时间隔为5ms,加15是为了修正重装所花费时间
//这个值可以通过软件仿真来确定,在这里设置断点,调整使两次运行
//时间差刚好为5ms即可。
 TH0=(65536-Fclk/1000/12*5+15)/256;	 
 TL0=(65536-Fclk/1000/12*5+15)%256;   // 

//LabZDebug_Start
	P2=1 << i;
	P1=zBuf[i];
	i++;
	if (i==4) {i=0;}
//LabZDebug_End  

}

 

Firmware下载 HID7Seg

PC端上位机主要代码

							int n=0;	
						 	 while (kbhit()==0)
								{
								WriteReportBuffer[1]=Seven_Dig[n / 1000 % 10];
								WriteReportBuffer[2]=Seven_Dig[n / 100 % 10];
								WriteReportBuffer[3]=Seven_Dig[n / 10 % 10];
								WriteReportBuffer[4]=Seven_Dig[n %10];

								printf("%d \n",n);

								//调用WriteFile函数发送数据
								Result=WriteFile(hUsb,
												WriteReportBuffer,
											    9,
											    &lpNumberOfBytesWritten,
										       	NULL);
								//如果函数返回失败,则可能是真的失败,也可能是IO挂起
								if(Result==FALSE)
										{
											//获取最后错误代码
												LastError=GetLastError();
											//看是否是真的IO挂		
												if((LastError==ERROR_IO_PENDING)||(LastError==ERROR_SUCCESS))
													{ return TRUE;	}
												//否则,是函数调用时发生错误,显示错误代码
												else	
													{
														printf("Sending error:%d \n",LastError);
														//如果最后错误为1,说明该设备不支持该函数。
														if(LastError==1)
														{printf("This device doesn't support WriteFile function \n");}
													}
										}

									n=(n++)%10000;

								}//while (kbhit()==0)

 

完整的代码下载 USB7Seg

参考:

1. http://www.lab-z.com/4digitial/ Arduino 四位数码管实验
2. http://www.geek-workshop.com/forum.php?mod=viewthread&tid=82&highlight=%CA%FD%C2%EB%B9%DC arduino学习笔记13 – 4位数码管实验