Step to UEFI (98)Shell加载EFI的分析

继续阅读《UEFI 原理与编程》,其中的第三章介绍了一个 EFI Application如何在 Shell下加载运行的。ShellProtocol.c中的InternalShellExecuteDevicePath 是具体实现这个功能的函数。
函数头:

/**
  internal worker function to load and run an image via device path.

  @param ParentImageHandle      A handle of the image that is executing the specified
                                command line.
  @param DevicePath             device path of the file to execute
  @param CommandLine            Points to the NULL-terminated UCS-2 encoded string
                                containing the command line. If NULL then the command-
                                line will be empty.
  @param Environment            Points to a NULL-terminated array of environment
                                variables with the format 'x=y', where x is the
                                environment variable name and y is the value. If this
                                is NULL, then the current shell environment is used.
                            
  @param[out] StartImageStatus  Returned status from gBS->StartImage.

  @retval EFI_SUCCESS       The command executed successfully. The  status code
                            returned by the command is pointed to by StatusCode.
  @retval EFI_INVALID_PARAMETER The parameters are invalid.
  @retval EFI_OUT_OF_RESOURCES Out of resources.
  @retval EFI_UNSUPPORTED   Nested shell invocations are not allowed.
**/
EFI_STATUS
EFIAPI
InternalShellExecuteDevicePath(
  IN CONST EFI_HANDLE               *ParentImageHandle,
  IN CONST EFI_DEVICE_PATH_PROTOCOL *DevicePath,
  IN CONST CHAR16                   *CommandLine OPTIONAL,
  IN CONST CHAR16                   **Environment OPTIONAL,
  OUT EFI_STATUS                    *StartImageStatus OPTIONAL
  )

 

第一步,先将 EFI Application加载到内存中,生成Image对象。取得这个对象的句柄为 NewHandle。

  //
  // Load the image with:
  // FALSE - not from boot manager and NULL, 0 being not already in memory
  //
  Status = gBS->LoadImage(
    FALSE,
    *ParentImageHandle,
    (EFI_DEVICE_PATH_PROTOCOL*)DevicePath,
    NULL,
    0,
    &NewHandle);

  if (EFI_ERROR(Status)) {
    if (NewHandle != NULL) {
      gBS->UnloadImage(NewHandle);
    }
    return (Status);
  }

第二步,取得命令行参数,将命令行的参数交给 NewHandle

 Status = gBS->OpenProtocol(
    NewHandle,
    &gEfiLoadedImageProtocolGuid,
    (VOID**)&LoadedImage,
    gImageHandle,
    NULL,
    EFI_OPEN_PROTOCOL_GET_PROTOCOL);

  if (!EFI_ERROR(Status)) {
    ASSERT(LoadedImage->LoadOptionsSize == 0);
    if (NewCmdLine != NULL) {
      LoadedImage->LoadOptionsSize  = (UINT32)StrSize(NewCmdLine);
      LoadedImage->LoadOptions      = (VOID*)NewCmdLine;
    }

 

看到这里我有一个问题:前面加载之后,为什么第二步就能够在被加载的Image上找到“EFI_Loaded_Image_Protocol”? 带着这样的问题追踪了代码。首先要找到 gBS->LoadImage 的真正实现代码,在\MdeModulePkg\Core\Dxe\DxeMain\DxeMain.c有定义,

//
// DXE Core Module Variables
//
EFI_BOOT_SERVICES mBootServices = {
  {
.................
.................
  (EFI_IMAGE_LOAD)                              CoreLoadImage,                            // LoadImage
.................
.................
}

 

再追CoreLoadImage代码在 \MdeModulePkg\Core\Dxe\Image\Image.c

/**
  Loads an EFI image into memory and returns a handle to the image.

  @param  BootPolicy              If TRUE, indicates that the request originates
                                  from the boot manager, and that the boot
                                  manager is attempting to load FilePath as a
                                  boot selection.
  @param  ParentImageHandle       The caller's image handle.
  @param  FilePath                The specific file path from which the image is
                                  loaded.
  @param  SourceBuffer            If not NULL, a pointer to the memory location
                                  containing a copy of the image to be loaded.
  @param  SourceSize              The size in bytes of SourceBuffer.
  @param  ImageHandle             Pointer to the returned image handle that is
                                  created when the image is successfully loaded.

  @retval EFI_SUCCESS             The image was loaded into memory.
  @retval EFI_NOT_FOUND           The FilePath was not found.
  @retval EFI_INVALID_PARAMETER   One of the parameters has an invalid value.
  @retval EFI_UNSUPPORTED         The image type is not supported, or the device
                                  path cannot be parsed to locate the proper
                                  protocol for loading the file.
  @retval EFI_OUT_OF_RESOURCES    Image was not loaded due to insufficient
                                  resources.
  @retval EFI_LOAD_ERROR          Image was not loaded because the image format was corrupt or not
                                  understood.
  @retval EFI_DEVICE_ERROR        Image was not loaded because the device returned a read error.
  @retval EFI_ACCESS_DENIED       Image was not loaded because the platform policy prohibits the 
                                  image from being loaded. NULL is returned in *ImageHandle.
  @retval EFI_SECURITY_VIOLATION  Image was loaded and an ImageHandle was created with a 
                                  valid EFI_LOADED_IMAGE_PROTOCOL. However, the current 
                                  platform policy specifies that the image should not be started.

**/
EFI_STATUS
EFIAPI
CoreLoadImage (
  IN BOOLEAN                    BootPolicy,
  IN EFI_HANDLE                 ParentImageHandle,
  IN EFI_DEVICE_PATH_PROTOCOL   *FilePath,
  IN VOID                       *SourceBuffer   OPTIONAL,
  IN UINTN                      SourceSize,
  OUT EFI_HANDLE                *ImageHandle
  )
{
  EFI_STATUS    Status;
  UINT64        Tick;
  EFI_HANDLE    Handle;

  Tick = 0;
  PERF_CODE (
    Tick = GetPerformanceCounter ();
  );

  Status = CoreLoadImageCommon (
             BootPolicy,
             ParentImageHandle,
             FilePath,
             SourceBuffer,
             SourceSize,
             (EFI_PHYSICAL_ADDRESS) (UINTN) NULL,
             NULL,
             ImageHandle,
             NULL,
             EFI_LOAD_PE_IMAGE_ATTRIBUTE_RUNTIME_REGISTRATION | EFI_LOAD_PE_IMAGE_ATTRIBUTE_DEBUG_IMAGE_INFO_TABLE_REGISTRATION
             );

  Handle = NULL; 
  if (!EFI_ERROR (Status)) {
    //
    // ImageHandle will be valid only Status is success. 
    //
    Handle = *ImageHandle;
  }

  PERF_START (Handle, "LoadImage:", NULL, Tick);
  PERF_END (Handle, "LoadImage:", NULL, 0);

  return Status;
}
在CoreLoadImageCommon 中可以找到加载安装EFI_Loaded_Image_Protocol 的代码。
  //
  //Reinstall loaded image protocol to fire any notifications
  //
  Status = CoreReinstallProtocolInterface (
             Image->Handle,
             &gEfiLoadedImageProtocolGuid,
             &Image->Info,
             &Image->Info
             );
  if (EFI_ERROR (Status)) {
    goto Done;
  }

 

就是说,在加载过程中,还要对Image 安装EFI_Loaded_Image_Protocol。因此,Load 的动作并不是简单的读取到内存中。

继续回到InternalShellExecuteDevicePath 代码中。

    //第三步,将修改好的ShellParamsProtocol 再安装到 Image上。
    //
    // Initialize and install a shell parameters protocol on the image.
    //
    ShellParamsProtocol.StdIn   = ShellInfoObject.NewShellParametersProtocol->StdIn;
    ShellParamsProtocol.StdOut  = ShellInfoObject.NewShellParametersProtocol->StdOut;
    ShellParamsProtocol.StdErr  = ShellInfoObject.NewShellParametersProtocol->StdErr;
    Status = UpdateArgcArgv(&ShellParamsProtocol, NewCmdLine, NULL, NULL);
    ASSERT_EFI_ERROR(Status);
    //
    // Replace Argv[0] with the full path of the binary we're executing:
    // If the command line was "foo", the binary might be called "foo.efi".
    // "The first entry in [Argv] is always the full file path of the
    //  executable" - UEFI Shell Spec section 2.3
    //
    ImagePath = EfiShellGetFilePathFromDevicePath (DevicePath);
    // The image we're executing isn't necessarily in a filesystem - it might
    // be memory mapped. In this case EfiShellGetFilePathFromDevicePath will
    // return NULL, and we'll leave Argv[0] as UpdateArgcArgv set it.
    if (ImagePath != NULL) {
      if (ShellParamsProtocol.Argv == NULL) {
        // Command line was empty or null.
        // (UpdateArgcArgv sets Argv to NULL when CommandLine is "" or NULL)
        ShellParamsProtocol.Argv = AllocatePool (sizeof (CHAR16 *));
        if (ShellParamsProtocol.Argv == NULL) {
          Status = EFI_OUT_OF_RESOURCES;
          goto UnloadImage;
        }
        ShellParamsProtocol.Argc = 1;
      } else {
        // Free the string UpdateArgcArgv put in Argv[0];
        FreePool (ShellParamsProtocol.Argv[0]);
      }
      ShellParamsProtocol.Argv[0] = ImagePath;
    }

    Status = gBS->InstallProtocolInterface(&NewHandle, &gEfiShellParametersProtocolGuid, EFI_NATIVE_INTERFACE, &ShellParamsProtocol);
    ASSERT_EFI_ERROR(Status);
//第四步,用 gBS->StartImage执行Image。
    //
    // now start the image and if the caller wanted the return code pass it to them...
    //
    if (!EFI_ERROR(Status)) {
      StartStatus      = gBS->StartImage(
                          NewHandle,
                          0,
                          NULL
                          );
      if (StartImageStatus != NULL) {
        *StartImageStatus = StartStatus;
      }

 

Step to UEFI (97)Shell下获得和设置环境变量

Shell 下可以通过 Set 命令获取和设置当前的环境变量。比如:在模拟环境下运行 Set 可以看到默认的 path 路径:
image001

这里介绍一下如何使用代码来实现这个功能。

首先介绍取得环境变量的函数,在 \ShellPkg\Include\Library\ShellLib.h 中可以看到他的原型。输入函数是要读取的环境变量名称。比如: path

/**
  Return the value of an environment variable.

  This function gets the value of the environment variable set by the
  ShellSetEnvironmentVariable function.

  @param[in] EnvKey             The key name of the environment variable.

  @retval NULL                  The named environment variable does not exist.
  @return != NULL               The pointer to the value of the environment variable.
**/
CONST CHAR16*
EFIAPI
ShellGetEnvironmentVariable (
  IN CONST CHAR16                *EnvKey
  );

 

其次,介绍一下设置环境变量的函数,同样在在 \ShellPkg\Include\Library\ShellLib.h 中可以看到他的原型。输入的参数是:环境变量名称,要设置的值,还有一个是该变量是否为易失的。如果设置为非易失,那么下次重新其中之后这个值还会存在Shell中。

/**
  Set the value of an environment variable.

  This function changes the current value of the specified environment variable. If the
  environment variable exists and the Value is an empty string, then the environment
  variable is deleted. If the environment variable exists and the Value is not an empty
  string, then the value of the environment variable is changed. If the environment
  variable does not exist and the Value is an empty string, there is no action. If the
  environment variable does not exist and the Value is a non-empty string, then the
  environment variable is created and assigned the specified value.

  This is not supported pre-UEFI Shell 2.0.

  @param[in] EnvKey             The key name of the environment variable.
  @param[in] EnvVal             The Value of the environment variable
  @param[in] Volatile           Indicates whether the variable is non-volatile (FALSE) or volatile (TRUE).

  @retval EFI_SUCCESS           The operation completed sucessfully
  @retval EFI_UNSUPPORTED       This operation is not allowed in pre-UEFI 2.0 Shell environments.
**/
EFI_STATUS
EFIAPI
ShellSetEnvironmentVariable (
  IN CONST CHAR16               *EnvKey,
  IN CONST CHAR16               *EnvVal,
  IN BOOLEAN                    Volatile
  );

 

编写一个测试的 Application

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

#include <Library/MemoryAllocationLib.h>
#include <Library/BaseMemoryLib.h>
#include <Protocol/UnicodeCollation.h>

#include  "ShellLib.h"

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_STATUS          Status;
  CONST CHAR16		  *Result;
  
  Result=ShellGetEnvironmentVariable(L"path");
  if (Result!=NULL) {
		Print(L"Get path=[%s]\n",Result);
	}
  
  Status=ShellSetEnvironmentVariable(L"labz1",L"100",TRUE);
  if (EFI_ERROR(Status)) {
		Print(L"Set Env. variable error\n",Result);
	}  

  Status=ShellSetEnvironmentVariable(L"labz2",L"200",FALSE);
  if (EFI_ERROR(Status)) {
		Print(L"Set Env. variable error\n",Result);
	}  
	
  Result=ShellGetEnvironmentVariable(L"labz1");
  if (Result!=NULL) {
		Print(L"Get labz1=[%s]\n",Result);
	}	
  
  return EFI_SUCCESS;
}

 

需要特别注意的地方是:在 NT32 模拟环境下,ShellSetEnvironmentVariable 函数无法使用:

image003
上面的代码首先显示一下当前的 path 变量,之后设置2个变量分别是 lab1=100和lab2=200.
下面的都是在实体机上进行测试的(测试平台:Kabylake HDK Board)

image005

运行之后我们再检查环境变量可以看到 labz1 和 labz2 已经设置进去了。
image006
重启之后,再次检查可以看到变量labz2仍然存在
image007
比如,某天你需要在BIOS中加入一个重启动测试的代码,可以考虑将重启次数放在环境变量中。

完整的代码下载 envtest

Putty 的辅助工具 V3.0

spc3V1.0
因为工作的关系需要取得串口信息来Debug。之前在XP下使用的一款软件(我也不清楚具体应该叫做什么名字,因为我下载时它的名字是“好用的串口工具.exe”),如同他的名字一样,确实很好用。但是令人郁闷的是自从切换到了Windows7 64位系统下,这个工具不再工作正常。于是又开始寻找起来新的工具。

后来用了一段时间的 Putty 发现功能强大。美中不足是每次打开操作复杂,首先双击之后会弹出权限的窗口,其次需要手动输入com号,而因为我使用USB串口,经常因为插拔而导致端口的变化,于是每次还要去设备管理器重查看目前的端口,打开设备管理器还会弹出权限窗口……..真的很麻烦。

因为Putty是开源软件,就想试图修改去掉限制。再后来发现Putty一起发布的有一个功能比较单一的PuttyTel,并且可以通过命令行方式进行端口的指定和调用,最后就编写这个软件。使用方法非常简单:使用时将这个文件和PuttyTel放在同一个目录下,然后运行之后双击当前端口列表中出现的需要串口即可立即调用起来。

源程序在Source目录下。特别注意这是Delphi XE4 下面Build通过的,并没有使用XE4的新特性,如果你使用其他版本Delphi需要修改对应的头文件。

V2.0 升级了一下,可以自己设置波特率,也可以兼容更多的能接受命令行参数的串口软件。

V2.1 上一个版本会确认一下当前的命令行参数,对于普通用户来说是多余的。删掉这个重新编译。另外,调整代码将 JediAPILib.inc 放在源程序的目录下。还有就是多显示一位版本号,之前只能显示大版本,比如:2 修改之后可以显示 2.1 这样的版本号。

V2.2 根据 krman@biosren的建议,增加如果配置文件中保存了一个端口,并且这个端口现在仍然在列表中,倒计时三秒后启动该端口。

V2.2 根据 krman@biosren的建议,增加如果配置文件中保存了一个端口,并且这个端口现在仍然在列表中,倒计时三秒后启动该端口。

V3.0 根据 heyjude@biosren的建议,增加Log的功能。勾选 ‘Log To File’之后,根据当前时间生成一个Log文件。另外,我更换掉之前的 PuttyTel 工具,因为这个工具不支持命令行指定 log 文件名。替换为 Putty.exe 。

参考:

1.Putty的官方网站(因为安全问题请不要使用国内的Putty软件。)
http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html

2.关于Putty命令行调用方式来自
http://tartarus.org/~simon/putty-snapshots/htmldoc/Chapter3.html
例如:-serial com4 -sercfg 115200,8,n,1,N

-sessionlog 文件名, 指定使用的 Log文件名

下载serialportchooseriii

Step to UEFI (96)Build 参数介绍

最近在认真研读《UEFI 原理与编程》,强烈推荐这本书,希望深入学习UEFI 的朋友都应该购买一本反复阅读。

《UEFI 原理与编程》第二章提到了build 命令的常用参数,我认真考察了一下。在 CMD窗口下运行 build –h 可以获得支持的命令参数列表:

–version 显示build的版本然后退出
-h, –help 显示帮助信息然后退出
-a TARGETARCH, –arch=TARGETARCH 指定编译目标,可选的有IA32, X64, IPF, ARM, AARCH64 或者 EBC。这个参数会覆盖 target.txt中指定的TARGET_ARCH。我们经常使用的 –a IA32 编译出来的 Application,更换为 –a X64即可编译出X64平台下可以用的 Application。
  -p PLATFORMFILE, –platform=PLATFORMFILE

 

 

指令要编译的 package。这个参数会覆盖 target.txt中指定的 ACTIVE_PLATFORM。
  -m MODULEFILE, –module=MODULEFILE 编译指定的模块。就是编译指定 INF文件。
  -b BUILDTARGET, –buildtarget=BUILDTARGET 指定编译为 RELASE版本还是 DEBUG版本。这个参数会覆盖 target.txt中指定的TARGET。
  -t TOOLCHAIN, –tagname=TOOLCHAIN

 

指定使用的工具集。这个参数会覆盖 target.txt中的TOOL_CHAIN_TAG 定义.

 

 

-x SKUID, –sku-id=SKUID

 

 

使用的给出的SKU ID来进行编译,覆盖DSC文件中的SKUID_IDENTIFIER (没用过。检查UDK2015 Source Code这个参数定义为 DEFAULT)
  -n THREADNUMBER 指定编译中使用的线程数量。理论上说线程越多,编译速度越快。这个参数会覆盖target.txt中的MAX_CONCURRENT_THREAD_NUMBER
  -f FDFFILE, –fdf=FDFFILE

 

指定 FDF的名称。这个参数会覆盖DSC文件中指定的FDF文件名。
  -r ROMIMAGE, –rom-image=ROMIMAGE 指定生成的 FD文件名称。这个名称必须是FDF文件中[FD] 节中存在的文件名。
  -i FVIMAGE, –fv-image=FVIMAGE

file.

指定生成的FV文件名称。这个名称必须是FDF文件中[FV] 节中存在的名称。
  -C CAPNAME, –capsule-image=CAPNAME

 

 

指定生成的Capsule文件名称。这个名称必须是FDF文件中[Capsule] 节中存在的名称。Capsule 是一种专门用来升级的UEFI定义的格式。比如:有对应的BIOS Capsule 文件,在启动过程中你把对应的文件放在U盘上,BIOS会自动识别并且加载然后升级自身,这样的好处是刷新环境稳定,避免操作系统的影响,避免被破解的风险。坏处是实际操中挺麻烦,需要准备FAT32格式的U盘。
  -u, –skip-autogen 跳过 Autogen 的过程进行编译
  -e, –re-parse 只做分析和生成编译需要的准备工作,不做实际编译,相当于只做AutoGen。
  -c, –case-insensitive 不检查 INF文件中的大小写
  -w, –warning-as-error 将 Warning视作 Error
-j LOGFILE, –log=LOGFILE

 

将编译过程中输出到屏幕上的信息同样输入到文件中。如果信息太多,无法看到期望的出错信息不妨试试这个参数直接查看Log文件。
  -s, –silent 沉默模式执行NMAKE。
  -q, –quiet           Disable all messages except FATAL ERRORS.

 

 

关闭除了致命错误之外的全部信息显示。感觉和上面的 –s没有差别。
-v, –verbose

 

打开 verbose模式(完整信息),包括 library实例,dependency表达式,warning信息等等。
  -d DEBUG, –debug=DEBUG

 

 

看起来是控制编译期的Debug信息输出。
  -D MACROS, –define=MACROS

 

定义宏
-y REPORTFILE, –report-file=REPORTFILE

Create/overwrite the report to the specified filename.

 

创建/覆盖指定的报告。这个功能非常有用,可以生成一个 report文件,其中包括了项目定义的 PCD变量等等的信息。这对于查看一些编译期的参数非常有用。
  -Y REPORTTYPE, –report-type=REPORTTYPE

 

 

创建/覆盖指定内容的报告。内容范围在[PCD, LIBRARY, FLASH, DEPEX, BUILD_FLAGS, FIXED_ADDRESS, EXECUTION_ORDER]。
  -F FLAG, –flag=FLAG  Specify the specific option to parse EDK UNI file.

Must be one of: [-c, -s]. -c is for EDK framework UNI

file, and -s is for EDK UEFI UNI file. This option can

also be specified by setting *_*_*_BUILD_FLAGS in

[BuildOptions] section of platform DSC. If they are

both specified, this value will override the setting

in [BuildOptions] section of platform DSC.

 

 

指定处理uni文件的方法,可选项是[-c,-s]。-c是用来处理EDK架构的UNI;-s用来处理EDK架构的 UNI。我不清楚具体有什么差别,如果你了解不妨告诉我
-N, –no-cache 关闭缓存机制。在EDK编译过程中是要生成一个小型的数据库文件的。我之前就遇到过杀毒软件会误杀这个数据库导致无法正常编译的情况。
–conf=CONFDIRECTORY 指定 Conf 的目录。
  –check-usage

 

检查 INF中提到的Source文件是否存在。只是检查而已,没有其他动作。
  –ignore-sources      Focus to a binary build and ignore all source files 不使用源程序,强制使用编译生成的二进制文件继续编译。

 

上述是通过简单实验和个人理解进行翻译的,如果有不妥当欢迎直接评论中指出或者给我写邮件。万分感谢!

 

 

2019年2月12日 补充。上面提到的 -Y 参数查看 PCD 设定的例子(UDK2018):

build -x X64 -Y PCD -y zz.txt

运行结束后在根目录下可以看到 zz.txt。需要注意的是:

1.建议先编译一次,确定能跑过再继续;

2.zz.txt 是编译结束的时候才生成的。

Arduino 使用固态继电器的例子

平时我们使用的继电器都是下面这种,内部是有机械结构来控制通断的。在工作的时候我们会听到“咔”的声响。
image002
如果你是持续的操作或者是在安静的环境下使用,这种声音就会非常恼人。此时可以考虑使用固态继电器,长相如下:
image004

注意,固态继电器有直流和交流的差别,上面这样一个差不多11元,比继电器模块贵一些(如果你的继电器模块功能比较全,有光耦之类的其实二者价格相差不多)。
上面图片显示的就是我入手的,控制端输入3-32V直流,被控制端可以控制5-60V直流。
具体线路连接如下:
image006

#define ControlPin 8
void setup()
{
    Serial.begin(9600);
    pinMode(ControlPin,OUTPUT);      //该端口需要选择有#号标识的数字口
    digitalWrite(ControlPin,LOW);
}

void loop()
{
  char  c;

    while (Serial.available() > 0)  
    {
        c=Serial.read();
        if (']'==c) 
          {
            digitalWrite(ControlPin,HIGH);
          }
        if ('['==c) 
          {
            digitalWrite(ControlPin,LOW);
          }

    }
}

 

最后放一下工作的照片
image007
总结【参考2】固态继电器的优缺点
1、固态继电器的优点
(1)高寿命,高可靠:SSR没有机械零部件,有固体器件完成触点功能,由于没有运动的零部件,因此能在高冲击,振动的环境下工作,由于组成固态继电器的元器件的固有特性,决定了固态继电器的寿命长,可靠性高。
(2)灵敏度高,控制功率小,电磁兼容性好:固态继电器的输入电压范围较宽,驱动功率低,可与大多数逻辑集成电路兼容不需加缓冲器或驱动器。
(3)快速转换:固态继电器因为采用固体其间,所以切换速度可从几毫秒至几微妙。
(4)电磁干扰笑:固态继电器没有输入”线圈”,没有触点燃弧和回跳,因而减少了电磁干扰。大多数交流输出固态继电器是一个零电压开关,在零电压处导通,零电流处关断,减少了电流波形的突然中断,从而减少了开关瞬态效应。
(5)无噪音

2、固态继电器的缺点
(1)导通后的管压降大,可控硅或双相控硅的正向降压可达1~2V,大功率晶体管的饱和压浆液灾1~2V之间,一般功率场效应管的导通电祖也较机械触点的接触电阻大。
(2)半导体器件关断后仍可有数微安至数毫安的漏电流,因此不能实现理想的电隔离。
(3)由于管压降大,导通后的功耗和发热量也大,大功率固态继电器的体积远远大于同容量的电磁继电器,成本也较高。
(4)电子元器件的温度特性和电子线路的抗干扰能力较差,耐辐射能力也较差,如不采取有效措施,则工作可靠性低。
(5)固态继电器对过载有较大的敏感性,必须用快速熔断器或RC阻尼电路对其进行过在保护。固态继电器的负载与环境温度明显有关,温度升高,负载能力将迅速下降。

另外,一般的继电器输出有3个Pin分别是:地,常开和常闭。固态继电器只有2个Pin,购买的时候就需要确定是常开的还是常闭。
参考:
1. http://www.lab-z.com/mos%E6%8E%A7%E5%88%B6%E5%B0%8F%E7%81%AF%E6%B3%A1%E7%9A%84%E5%AE%9E%E9%AA%8C/ MOS控制小灯泡的实验
2. http://www.elecfans.com/yuanqijian/jidianqi/20121030295690.html 固态继电器简介及优缺点

Step to UEFI (95)又一个截图软件

之前我写过一个UEFI 截图软件,功能有限,最近在网上看到了一个开源的截图软件【参考1】,支持热键,抓图结果会被转化为 Png格式,自动存在FSx:下面。于是,下载编译实验之。

代码中lodepng.h 和qsort.c 中对于size_t 的定义有些问题,看起来他是想使用VS默认的定义,但是不知道为什么我在 UDK2015 + Vs2013 下面编译会有问题,重新定义一下,编译就OK了。

编译好的代码无法在NT32模拟环境下运行。下面是在平板电脑的 Shell 中运行的结果,使用 Load 命令加载之后就可以使用 ctrl+alt+F12截屏:

image001

image002

之后在Intel  Kabylake HDK上测试

image003

进入 RU 之后还可以正常工作

image004

除了一般的Shell下截图,还可以先进入 Shell 加载之后再退出到Setup中,同样的热键截图

image005

修改之后的源代码在这里:

CrScreenshotDxe

参考:

1.项目地址在 https://github.com/LongSoft/CrScreenshotDxe

2023年5月6日

编译后的 X64 版本EFI程序可以在这里下载

给麦步手表写一个获取比特币行情的表盘

我对手表有着深厚的感情,每当人生遇到坎坷时,或者愤怒不已的时候,我都会想象和安慰自己“我去年买了块表”。刚有taobao的时候,我买了一块卡西欧的太阳能手表,时至今日我还记得价格是 258元。十多年过去了,这块表的卖家已经消失,我从来没有给他更换过电池,这块手表依然行走完好。
image001

古人云“穷玩车,富玩表”。为了体验一下富人的感觉,最近入手了一块麦步智能手表。对于这块手表,最重要的是可以写程序。于是我就尝试给他编写程序,听上去又在重复“屌丝玩电脑”。

image002

首先通读API,了解这个手表的原理。简单的说,这块手表通过蓝牙连接,从而实现WIFI通讯。
image003

目标是编写一个获取当前比特币行情。huobi 网提供了行情API, http://api.huobi.com/staticmarket/ticker_btc_json.js 可以显示实时行情。例如:一次返回数据如下

{“time”:”1467817886″,”ticker”:{“open”:4479,”vol”:604962.2546,”symbol”:”btccny”,”last”:4522.79,”buy”:4522.79,”sell”:4523.03,”high”:4612,”low”:4467.64} }。 其中的 last 就是当前的价格。

配合麦步手表提供的显示股票行情的代码,编写自己的程序如下:

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

 

#include "maibu_sdk.h"

#include "maibu_res.h"

 

 

/*Web通讯ID*/

static uint32_t g_comm_id_web = 0;

 

/*Web request地址*/

#define DATA_WEB     "http://api.huobi.com/staticmarket/ticker_btc_json.js"

 

/* 时间项 */

#define DATE_TEXT_POS_X                      2

#define DATE_TEXT_POS_Y                      6

#define DATE_TEXT_SIZE_W                     70

#define DATE_TEXT_SIZE_H                     14

 

#define TIME_TEXT_POS_X                      90

#define TIME_TEXT_POS_Y                      6

#define TIME_TEXT_SIZE_W                     36

#define TIME_TEXT_SIZE_H                     14

 

static const char weekday[7][11] =

{

    {"周日"},

    {"周一"},

    {"周二"},

    {"周三"},

    {"周四"},

    {"周五"},

    {"周六"}

};

 

 

/*窗口ID*/

static int32_t g_window_id = -1;

 

//数据提示

static int32_t g_layer_id_text = -1;

//数据内容

static int32_t g_layer_id_data = -1;

 

//时间的句柄

static int32_t g_layer_id_time   = -1;

//日期的句柄

static int32_t g_layer_id_date   = -1;

 

//整个窗体句柄

static P_Window h_window;

 

void data_request_web()

{

     /*拼接url请求地址, 注意url的缓存大小*/

     char url[200] = "";   

     sprintf(url, "%s", DATA_WEB);              

 

     /*

               拼接过滤参数,即只接受和过滤参数匹配的返回值

               个人感觉这里的过滤可能是让手机做的,就是这里通知手机:json中的数据除了我制定的其他都过滤掉

     */

 

     /*发送一次*/

     g_comm_id_web = maibu_comm_request_web(url, "last", 0);

}

 

void add_text_layer(P_Window p_window, int32_t *p_layer_id, char *p_str, GRect *p_frame, enum GAlign align, int8_t font, enum GColor color)

{

    LayerText text_cfg = {p_str, *p_frame, align, font, 0};

    P_Layer layer = app_layer_create_text(&text_cfg);

    app_layer_set_bg_color(layer, color);

 

    P_Layer old_layer = app_window_get_layer_by_id(p_window, *p_layer_id);

    if(old_layer)

    {

        *p_layer_id = app_window_replace_layer(p_window, old_layer, layer);

    }

    else

    {

        *p_layer_id = app_window_add_layer(p_window, layer);

    }

}

 

static void add_time_bar(P_Window p_window)

{

    /* 添加时间图层 */

    uint8_t text_buf[40];

    struct date_time t;

    app_service_get_datetime(&t);

 

    memset(text_buf, 0, sizeof(text_buf));

    sprintf((char *)text_buf, "%s", (char *)&weekday[t.wday][0]);

    sprintf(&text_buf[6], "%02d-%02d", t.mon, t.mday);

 

    GRect frame;

    frame.origin.x = DATE_TEXT_POS_X;

    frame.origin.y = DATE_TEXT_POS_Y;

    frame.size.h   = DATE_TEXT_SIZE_H;

    frame.size.w   = DATE_TEXT_SIZE_W;

 

    add_text_layer(p_window, &g_layer_id_date, (char*)text_buf, &frame, GAlignLeft, U_ASCII_ARIAL_14, GColorWhite);

 

    frame.origin.x = TIME_TEXT_POS_X;

    frame.origin.y = TIME_TEXT_POS_Y;

    frame.size.h   = TIME_TEXT_SIZE_H;

    frame.size.w   = TIME_TEXT_SIZE_W;

 

    memset(text_buf, 0, sizeof(text_buf));

    sprintf(text_buf, "%02d:%02d", t.hour, t.min);

 

    add_text_layer(p_window, &g_layer_id_time, (char*)text_buf, &frame, GAlignLeft, U_ASCII_ARIAL_14, GColorWhite);

}

 

P_Window init_btc_window()

{

     static P_Window p_window;

    

     p_window = app_window_create();

     if (NULL == p_window)

     {

               return NULL;

     }

 

    /* 添加表盘背景 */

    GRect frame = {{0, 0}, {128, 128}};

    GBitmap bitmap;

 

    res_get_user_bitmap(BMP_STOCK_BG, &bitmap);

    LayerBitmap layer_bitmap = {bitmap, frame, GAlignCenter};

    P_Layer layer = app_layer_create_bitmap(&layer_bitmap);

    app_window_add_layer(p_window, layer);

 

    /* 添加时间栏 */

    add_time_bar(p_window);

 

     /*加入你的代码 begin*/     

 

     /*添加数据提示信息*/

     GRect frame_main = {{0, 30}, {16, 128}};

    add_text_layer(p_window, &g_layer_id_text, "BTC", &frame_main, GAlignCenter, U_ASCII_ARIAL_14, GColorWhite);

    

     /*添加数据*/

     GRect frame_data = {{0, 60}, {16, 128}};

    add_text_layer(p_window, &g_layer_id_data, "waiting", &frame_data, GAlignCenter, U_ASCII_ARIAL_14, GColorWhite);

    

     return p_window;

 

}

 

void data_comm_result_callback(enum ECommResult result, uint32_t comm_id, void *context)

{

 

     /*如果上一次请求WEB通讯失败,并且通讯ID相同,则重新发送*/

     if ((result == ECommResultFail) && (comm_id == g_comm_id_web))

     {

               data_request_web();

     }

    

}

 

static void web_recv_callback(const uint8_t *buff, uint16_t size)

{

    char stock_gid[10];

     char i;

 

    maibu_get_json_str(buff, "last", stock_gid, 10);

 

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

     {

     if (stock_gid[i]=='}')

               {

                         stock_gid[i]=0;

               }

     }

 

     /*添加数据*/

     GRect frame_data = {{0, 60}, {16, 128}};

    add_text_layer(h_window, &g_layer_id_data, stock_gid, &frame_data, GAlignCenter, U_ASCII_ARIAL_14, GColorWhite);

    

     app_window_update(h_window);

}

 

static void watch_time_change_callback(enum SysEventType type, void *context)

{

    /*时间更改,分变化*/

    if (type == SysEventTypeTimeChange)

    {

        uint8_t text_buf[40];

        struct date_time t;

        app_service_get_datetime(&t);

 

        memset(text_buf, 0, sizeof(text_buf));

        sprintf((char *)text_buf, "%s", (char *)&weekday[t.wday][0]);

        sprintf(&text_buf[6], "%02d-%02d", t.mon, t.mday);

 

        GRect frame;

        frame.origin.x = DATE_TEXT_POS_X;

        frame.origin.y = DATE_TEXT_POS_Y;

        frame.size.h   = DATE_TEXT_SIZE_H;

        frame.size.w   = DATE_TEXT_SIZE_W;

 

        add_text_layer(h_window, &g_layer_id_date, (char*)text_buf, &frame, GAlignLeft, U_ASCII_ARIAL_14, GColorWhite);

 

        frame.origin.x = TIME_TEXT_POS_X;

        frame.origin.y = TIME_TEXT_POS_Y;

        frame.size.h   = TIME_TEXT_SIZE_H;

        frame.size.w   = TIME_TEXT_SIZE_W;

 

        memset(text_buf, 0, sizeof(text_buf));

        sprintf(text_buf, "%02d:%02d", t.hour, t.min);

 

        add_text_layer(h_window, &g_layer_id_time, (char*)text_buf, &frame, GAlignLeft, U_ASCII_ARIAL_14, GColorWhite);

 

        app_window_update(h_window);

    }

}

 

static void data_timer_callback(date_time_t tick_time, uint32_t millis, void *context)

{

    data_request_web();

}

 

int main()

{

 

     /*创建消息列表窗口*/

     h_window = init_btc_window();

 

     //订阅时间改变事件

    maibu_service_sys_event_subscribe(watch_time_change_callback);

    

     /*放入窗口栈显示*/

     g_window_id = app_window_stack_push(h_window);

 

     data_request_web();

 

     /*注册通讯结果回调*/

     maibu_comm_register_result_callback(data_comm_result_callback);

 

     //注册网络请求回调函数

    maibu_comm_register_web_callback(web_recv_callback);

 

    //聚合数据源每隔10s更新数据

    app_window_timer_subscribe(h_window, 10000, data_timer_callback, (void *)h_window);

    

     return 0;

}

 

编译之后上传到手表上就OK了。

看起来工作正常,美中不足就是屏幕有点小,如果能像美国队长那块一样大就完美了。

image004

这块手表的一大重要特性就是采用电纸屏幕,就是那种 Kindle 的屏幕材质,不改变内容无需耗电,这样待机可以很长时间(标称21天)。说道这里,我想起来一个很老的苏联笑话:

有一个人在机场等六点的飞机,可是他忘记了带手表,于是他想找

个人问问。这时,他看见一个人提着两个巨大的手提箱吃力的走过来,手腕上戴

着一块异常漂亮的一看就知道是高科技产品的手表。

  “请问,几点了?”他问道。

  “哪个国家的时间?”那人反问。

  “哦?”他的好奇心来了,“你都知道哪些国家的时间呢?”

  “所有的国家,”那人回答道。

  “哇!那可真是一块好手表呀!”

  “还不止这些呢,这块表还有GPS卫星系统,可以随时收发电子邮件、

传真,这个彩色的屏幕可以收看NTSC制式的电视节目!”那人给他

演示,果真如此!

“哦!太棒了,我真想拥有一块这样的手表,您……您可以把它卖给我吗?”

“说实话,我已经烦透了这块表了,这样吧,900美元,如何?”

  他马上掏出支票簿,写了900美元给那人,“成交!”

 “好的,现在,它是你的了。”那人如释重负,把手表交给他,“这个是你

的手表”,然后指着地上的两个大箱子说,“这两个是电池!”

无数的科学家和工程师在不断的努力改造着我们的生活。

好玩的电子仪器(1)

本系列文章将会介绍一些电子工程师用到的“奇怪”设备。
对于我这样的Firmware工程师来说,硬件充满了神秘,而对于硬件工程师来说,射频更加神秘。
这几天组里的WIFI工程师在摆弄一台机器,好奇之下,我和他聊了一下,他介绍说这是一台无线信号分析器。能够捕获和分析无线信号。
image002

好奇之下,我第二天拿了一个玩具的遥控器给他请他帮忙分析。
image004

他先是询问了一下大概的频率,我说不清楚可能是 315M 的。他就设定扫描100-400M 的频率范围,我一边拨动开关让遥控器工作起来,设备就开始扫描和记录这个范围内的无线信号。很快,我们就在 108Mhz 上看到了信号。之后他又设定了 0-200Mhz作为扫描范围。结果发现,真正的信号是 54Mhz,刚才看到的108Mhz只是一个倍频。结果就是下面这个图片。

image006

后来偶然间我把遥控器翻了过来,上面贴着标签,赫然写着“54Mhz”,看起来这个遥控器频率确实是 54Mhz。
之后我又和他聊了一下,我问他是否有可能用这个设备分析出具体的信号,然后我可以用其他发射器做一个遥控器。他说无法做到,因为这个设备只能用来扫描载波的频率,而实际上有着多种多样的调制方式。比如,我们可以用这个设备来看到2.4G 鼠标和USB3.0 在频率段上冲突,但是具体的冲突方式是无法看到的。
我们实验室是开放的,在刚才的测试过程中还乱入了一个100Mhz 的信号(箭头所示)。真正的分析应该是在铁皮包裹的无线电屏蔽室完成的。比如,之前我在的一家公司,产线生产出来的蓝牙模块要到这样屏蔽室来完成最后的调整和测试。
image008

最后再补充一个八卦的故事,讲讲这个屏蔽室的故事。前面说了,我之前工作的一家公司为了测试蓝牙,弄了个屏蔽室。这个房间很小,建在公司一个大办公室的中间的,10个平方,或者说30个立方更合适,估计有厨房或者厕所那么大。这种专业性很强的事情只能找专业的公司做,台湾人也有相互扶助的传统,找了另外一家台湾公司。建好了之后,承建方发现最大的问题是收不上来钱,每次要钱财务总是说等研发验收或者托词主管不在。

貌似拖欠款项是我之前那家公司的传统,原因是拖欠的货款就是自己的利润,打个比方:拖欠1万,存在余额宝,每天就是八毛多,老板可以多吃一个鸡蛋。如果拖欠10万,那么老板的早餐就有着落了。我在的时候遇到过几次元件供应商因为收不上钱而停止供货的事情。老板就会压着研发换物料。据说之前还发生过逢年过节,供应商开着卡车堵大门的事情,只是我没有碰到过,想必也很热闹。

后来,承建屏蔽室的哥们怒了,直接打电话给财务,说我叔叔是“立委”的 xxx,如果我明天还看不到钱,我就召开记者发布会让你们上《苹果日报》。财务接电话的不是台湾人,琢磨半天“立痿”是什么毛病。后来和他老板汇报了一下,老板急忙查了一下,真有这么个人,也甭管是三叔二叔还是表叔,自己理亏,还是把钱还了吧。第二天,财务主管主动打电话过去主动道歉,跪舔一番,欠款如数奉上,这个事情就了结了。

再后来,还是这个屏蔽室,测试蓝牙,买了一套软件能够自动扫频率和调教模块。结果又是欠对方钱,双方扯皮,只是对方早有防备,软件中加了一个什么逻辑锁,试用期结束后不能自动测试。老板灵机一动,找一批操作工三班倒,简单培训后就上岗,用设备纯手工测试,节省环保还智能……….

有诗为证:

小小公司真奇妙,
老板看着似土豪。
怎料长欠不愿还,
欲想追债只能闹。

自动解压ZIP的批处理

比如,我在一个目录下有大量的 zip 文件,我想解压之,就可以使用下面的批处理

for %%i in (*.zip) do 7z x %%i -o%%~ni

其中:

for %%i in (*.zip) 是枚举目录下的全部 zip 文件名

7z x %%i 是按照完整路径解压的意思

-o%%~ni 是解压到文件名相同的目录的意思

比如,我有一个zip文件名称是 labz.zip 那么运行之后,会生成一个 labz 的目录,然后将文件内容解压进去