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 (36) —– 枚举Shell下的全部盘符

目标:写一个小程序来枚举当前系统中的盘符。比如:FS0: FS1: 等等。

和这个需求最相近的参考文件是 map 功能,每次启动shell的时候他都会展示一下当前系统中的全部盘符。这个功能的代码在 ShellPkg\Library\UefiShellLevel2CommandsLib\Map.c 。大概研究了一下,实现的方法是分别枚举有 Simple File Protocol 和 Block IO Protocol 的Handle (在 PerformMappingDisplay 函数中),然后取每的 Device Path Protocol(在 PerformSingleMappingDisplay 函数中),最后从这个Protocol中获取对应的盘符。

map.c中使用 gEfiShellProtocol->GetMapFromDevicePath 功能取得名称,但是在实际测试过程中我的程序中取得到的 gEfiShellProtocol 不知为何一直为0. 最后只得使用 mEfiShellEnvironment2 -> GetFsName 来完实现这个功能。

ShellPkg\Include\Protocol\EfiShellEnvironment2.h 有 EFI_SHELL_ENVIRONMENT2的定义

/// EFI_SHELL_ENVIRONMENT2 protocol structure.
typedef struct {
  SHELLENV_EXECUTE                        Execute;
  SHELLENV_GET_ENV                        GetEnv;
  SHELLENV_GET_MAP                        GetMap;
  SHELLENV_ADD_CMD                        AddCmd;
  SHELLENV_ADD_PROT                       AddProt;
  SHELLENV_GET_PROT                       GetProt;
                          CurDir;
  SHELLENV_FILE_META_ARG                  FileMetaArg;
  SHELLENV_FREE_FILE_LIST                 FreeFileList;

  //
  // The following services are only used by the shell itself.
  //
  SHELLENV_NEW_SHELL                      NewShell;
  SHELLENV_BATCH_IS_ACTIVE                BatchIsActive;

  SHELLENV_FREE_RESOURCES                 FreeResources;

  //
  // GUID to differentiate ShellEnvironment2 from ShellEnvironment.
  //
  EFI_GUID                                SESGuid;
  //
  // Major Version grows if shell environment interface has been changes.
  //
  UINT32                                  MajorVersion;
  UINT32                                  MinorVersion;
  SHELLENV_ENABLE_PAGE_BREAK              EnablePageBreak;
  SHELLENV_DISABLE_PAGE_BREAK             DisablePageBreak;
  SHELLENV_GET_PAGE_BREAK                 GetPageBreak;

  SHELLENV_SET_KEY_FILTER                 SetKeyFilter;
  SHELLENV_GET_KEY_FILTER                 GetKeyFilter;

  SHELLENV_GET_EXECUTION_BREAK            GetExecutionBreak;
  SHELLENV_INCREMENT_SHELL_NESTING_LEVEL  IncrementShellNestingLevel;
  SHELLENV_DECREMENT_SHELL_NESTING_LEVEL  DecrementShellNestingLevel;
  SHELLENV_IS_ROOT_SHELL                  IsRootShell;

  SHELLENV_CLOSE_CONSOLE_PROXY            CloseConsoleProxy;
  HANDLE_ENUMERATOR                       HandleEnumerator;
  PROTOCOL_INFO_ENUMERATOR                ProtocolInfoEnumerator;
  GET_DEVICE_NAME                         GetDeviceName;
  GET_SHELL_MODE                          GetShellMode;
  SHELLENV_NAME_TO_PATH                   NameToPath;
  SHELLENV_GET_FS_NAME                    GetFsName;
  SHELLENV_FILE_META_ARG_NO_WILDCARD      FileMetaArgNoWildCard;
  SHELLENV_DEL_DUP_FILE                   DelDupFileArg;
  SHELLENV_GET_FS_DEVICE_PATH             GetFsDevicePath;
} EFI_SHELL_ENVIRONMENT2;

 

同一个文件中

/**
  Converts a device path into a file system map name.

  If DevPath is NULL, then ASSERT.

  This function looks through the shell environment map for a map whose device
  path matches the DevPath parameter.  If one is found the Name is returned via
  Name parameter.  If sucessful the caller must free the memory allocated for
  Name.

  This function will use the internal lock to prevent changes to the map during
  the lookup operation.

  @param[in] DevPath                The device path to search for a name for.
  @param[in] ConsistMapping         What state to verify map flag VAR_ID_CONSIST.
  @param[out] Name                  On sucessful return the name of that device path.

  @retval EFI_SUCCESS           The DevPath was found and the name returned
                                in Name.
  @retval EFI_OUT_OF_RESOURCES  A required memory allocation failed.
  @retval EFI_UNSUPPORTED       The DevPath was not found in the map.
**/
typedef
EFI_STATUS
(EFIAPI *SHELLENV_GET_FS_NAME) (
  IN EFI_DEVICE_PATH_PROTOCOL     * DevPath,
  IN BOOLEAN                      ConsistMapping,
  OUT CHAR16                      **Name
  );

 

根据上面的函数,编写程序如下

#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;

EFI_STATUS
EFIAPI
PerformSingleMappingDisplay(
  IN CONST EFI_HANDLE Handle
  )
{
  EFI_DEVICE_PATH_PROTOCOL  *DevPath;
  EFI_DEVICE_PATH_PROTOCOL  *DevPathCopy;
  CHAR16                    *CurrentName;

  CurrentName = NULL;
  DevPath = DevicePathFromHandle(Handle);
  DevPathCopy = DevPath;
  mEfiShellEnvironment2->GetFsName(DevPathCopy,FALSE,&CurrentName);

  Print (L"%s \r\n", CurrentName);  
	
  if ((CurrentName) != NULL) { FreePool((CurrentName)); CurrentName = NULL; }

  return EFI_SUCCESS;
}

int
EFIAPI
main (                                         
  IN int Argc,
  IN char **Argv
  )
{
  EFI_STATUS                Status;
  EFI_HANDLE                *HandleBuffer=NULL;
  UINTN                     BufferSize=0;
  UINTN                     LoopVar;
  BOOLEAN                   Found;
  //Copy from ShellLibConstructorWorker in \ShellPkg\Library\UefiShellLib\UefiShellLib.c
  //
  // UEFI 2.0 shell interfaces (used preferentially)
  //
  Status = gBS->OpenProtocol(
    gImageHandle,
    &gEfiShellProtocolGuid,
    (VOID **)&gEfiShellProtocol,
    gImageHandle,
    NULL,
    EFI_OPEN_PROTOCOL_GET_PROTOCOL
   );
   
  if (EFI_ERROR(Status)) {
    //
    // Search for the shell protocol
    //
    Status = gBS->LocateProtocol(
      &gEfiShellProtocolGuid,
      NULL,
      (VOID **)&gEfiShellProtocol
     );
    if (EFI_ERROR(Status)) {
      gEfiShellProtocol = NULL;
     }
  }
  
  //
  // Look up all SimpleFileSystems in the platform
  //
  Status = gBS->LocateHandle(
    ByProtocol,
    &gEfiSimpleFileSystemProtocolGuid,
    NULL,
    &BufferSize,
    HandleBuffer);
	
  if (Status == EFI_BUFFER_TOO_SMALL) {
		HandleBuffer = AllocateZeroPool(BufferSize);
		if (HandleBuffer == NULL) {
			return (SHELL_OUT_OF_RESOURCES);
		}
		Status = gBS->LocateHandle(
			ByProtocol,
			&gEfiSimpleFileSystemProtocolGuid,
			NULL,
			&BufferSize,
			HandleBuffer);
   }

  //
  // Get the map name(s) for each one.
  //
  for ( LoopVar = 0, Found = FALSE
      ; LoopVar < (BufferSize / sizeof(EFI_HANDLE)) && HandleBuffer != NULL
      ; LoopVar ++
     ) {
    Status = PerformSingleMappingDisplay(HandleBuffer[LoopVar]);
    if (!EFI_ERROR(Status)) {
      Found = TRUE;
    }
  }
  
  FreePool(HandleBuffer);
	
  return EFI_SUCCESS;
}

 

运行结果,和 Map命令的对比

ShowMap

完整的代码下载
ShowMap

实验 MPU-6050模块

在使用之前,需要特别注意的是:MPU-6050 本身只支持到 3.3V,如果你需要供电5V那么要特别确认一下你的板子是否有转换。我使用的下面这个模块上面带降压的,所以可以直接接5V.

T1OIRvXBXfXXXXXXXX_!!0-item_pic

另外,连接GND和VCC时一定要注意不能搞反了,否则转换芯片会急剧发热(推荐你第一次上电的时候留心这个芯片)。

首推极客工坊的文章【参考1】。但是可能是版本太老的缘故,在编译时(1.5.0)会出现core.a的错误。

随后查了一下,Arduino官方网站上有介绍和示例代码【参考2】:

// MPU-6050 Short Example Sketch
// By Arduino User JohnChi
// August 17, 2014
// Public Domain
#include<Wire.h>
const int MPU=0x68;  // I2C address of the MPU-6050
int16_t AcX,AcY,AcZ,Tmp,GyX,GyY,GyZ;
void setup(){
  Wire.begin();
  Wire.beginTransmission(MPU);
  Wire.write(0x6B);  // PWR_MGMT_1 register
  Wire.write(0);     // set to zero (wakes up the MPU-6050)
  Wire.endTransmission(true);
  Serial.begin(9600);
}
void loop(){
  Wire.beginTransmission(MPU);
  Wire.write(0x3B);  // starting with register 0x3B (ACCEL_XOUT_H)
  Wire.endTransmission(false);
  Wire.requestFrom(MPU,14,true);  // request a total of 14 registers
  AcX=Wire.read()<<8|Wire.read();  // 0x3B (ACCEL_XOUT_H) & 0x3C (ACCEL_XOUT_L)     
  AcY=Wire.read()<<8|Wire.read();  // 0x3D (ACCEL_YOUT_H) & 0x3E (ACCEL_YOUT_L)
  AcZ=Wire.read()<<8|Wire.read();  // 0x3F (ACCEL_ZOUT_H) & 0x40 (ACCEL_ZOUT_L)
  Tmp=Wire.read()<<8|Wire.read();  // 0x41 (TEMP_OUT_H) & 0x42 (TEMP_OUT_L)
  GyX=Wire.read()<<8|Wire.read();  // 0x43 (GYRO_XOUT_H) & 0x44 (GYRO_XOUT_L)
  GyY=Wire.read()<<8|Wire.read();  // 0x45 (GYRO_YOUT_H) & 0x46 (GYRO_YOUT_L)
  GyZ=Wire.read()<<8|Wire.read();  // 0x47 (GYRO_ZOUT_H) & 0x48 (GYRO_ZOUT_L)
  Serial.print("AcX = "); Serial.print(AcX);
  Serial.print(" | AcY = "); Serial.print(AcY);
  Serial.print(" | AcZ = "); Serial.print(AcZ);
  Serial.print(" | Tmp = "); Serial.print(Tmp/340.00+36.53);  //equation for temperature in degrees C from datasheet
  Serial.print(" | GyX = "); Serial.print(GyX);
  Serial.print(" | GyY = "); Serial.print(GyY);
  Serial.print(" | GyZ = "); Serial.println(GyZ);
  delay(333);
}

 

我使用的是 Arduino Pro Micro (这并非Arduino官方出品,是一种类似 Arduino Leonardo 的兼容产品) , 电路非常简单,使用面包板即可

6050

运行结果

6050r

取得值本身很简单,但是如果想解出正确的姿态还是蛮复杂的。

参考:

1.http://www.geek-workshop.com/thread-1017-1-1.html arduino学习笔记37 – Arduino Uno + MPU6050首例整合性6轴演示实验

2.http://playground.arduino.cc/Main/MPU-6050 MPU-6050 Accelerometer + Gyro

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起来。

Arduino Uno 的 Vin Pin

偶然注意到 Arduino Uno 上有个Vin Pin,但是AVR上没有对应的管脚,感觉比较奇怪,研究了一下。

ArduinoVin

原始图片来自【参考1】

这样的问题自然是要到电路图中查找答案。电路图和印刷版文件来自【参考2】

vin2

美中不足:虽然有PDF的电路图,但是不支持搜索,找了半天才在右上角找到

vin3

就是说Vin可以看作是DC输入的电压经过了一个二极管。因此,有两种情况:一是如果你插入了DC,那么Vin上会出现比DC稍微小一点的电压(因为经过了一个二极管);第二种情况,可以直接从这里灌进去一个电压。猜测这样的设计可能是为了某些有供电能力的Shield考虑吧。

关于这部分,有一些介绍【参考2】

External (non-USB) power can come either from an AC-to-DC adapter (wall-wart) or battery. The adapter can be connected by plugging a 2.1mm center-positive plug into the board’s power jack. Leads from a battery can be inserted in the Gnd and Vin pin headers of the POWER connector.

再查看 PCB 文件

vin31

对照 Vin 可以看到,它是接U1的 3 Pin的。

vin41

U1是 NCP1117ST50T3,简单翻翻手册【参考3】,这是一个降压的IC,Pin 3是输入

vin6

因此,Vin是一个可以直接对板子供电,电器特性功能和板子上DC插孔类似的输入脚。

参考:

1.http://arduino.cc/en/uploads/Main/ArduinoUno_R3_Front.jpg

2.http://arduino.cc/en/Main/ArduinoBoardUno

3.http://wenku.baidu.com/view/f66976af284ac850ad0242b9.html NCP1117

4.http://solderpad.com/solderpad/arduino-uno/ Bom List特别注意上面的一些名称和原本电路图有差别。

二维码对联

前一段看张鸣老师的书,上面提到对联是一种很重要的传统文化表现形式,尤其是春节时要四处张贴的春联。据说很多地区的大学生春节回家之后还要承担起为乡亲们题写春联的重任。母亲说当年他们插队时,逢年也会给老乡写春联。内容上通常是毛语录,比如:“金猴奋起千钧棒,玉宇澄清万里埃”或者“四海翻腾云水怒 五洲震荡风雷激”,一类的。

每年春节上街我都会觉得这是越来越重要的传统——至少从春联的价格上来看是这样。让我编点春联是没问题的,至于写的话因为现在提笔忘字以及本人的字都会被老婆笑话,还是免谈了。

作为一个专业的工程技术人员,灵机一动,想到了既然我只能编不能写可以用眼下最时髦的二维码技术来生成。有好奇心的人就会去扫一扫,然后就能看到了。具体做法如下:

首先,找个二维码生成软件。选择最高的误码率,简单的理解这个就是你能够覆盖生成二维码的比率。比如:25%的误码率意味着你可以覆盖画面上25%的区域仍然能够正常扫出。

错误修正容量

L水平 7%的字码可被修正

M水平 15%的字码可被修正

Q水平 25%的字码可被修正

H水平 30%的字码可被修正

【参考 1】

image001

【参考1】

我随便选了一个软件,二维码大师,最高误码率30%,然后选择最高分辨率。文本内容中输入上联“壹零做法神通广大不服来扫我”,输入过程中就自动生成二维码了,具体在下面三个地方设置和输入。

image002

选择保存为一个BMP文件。比如上面“壹零做法神通广大不服来扫我”,生成的结果就是下面的图片。

image003

然后修改这个图片即可,我最喜欢用的就是Windows的画笔工具。

image004

修改之后结果

image005

特别的,修改之后一定要用刚才的软件校验一下,避免做完了根本无法识别的状况。

image006

同样做法,再做一下下联“纵横成形网挂众多若信任联”。

image007
上面这个对联是我父亲拟的,横批就是“二维码”。

最后多说两句,大多数情况下人们只是注意上下问对仗是否工整而不大在意内容本身,好比,有女生给班主任写条子,”老师,我怀了男朋友得孩子”。老师看到之后大吃一惊“唉,现在的学生啊,真是的!都上初中了,怎么还分不清楚‘的地得’的用法?”。我随口拟了一个联儿,念给父亲听,他听了片刻说“对的比较工整,这是一个传统的联吧?”

image008

image009

参考:

1. http://zh.wikipedia.org/wiki/QR%E7%A2%BC QR码

Windows 7 64位下arduino驱动安装失败解决办法

最近入手一块 Arduino Micro Pro ,插在台式机上之后要求安装驱动,指定了 Arduino 目录下的 Drivers之后还出现了 Inf 段落无效的字样。忽然想起来我的 Win7 是Ghost版本,于是上网搜索解决办法。

在 http://blog.csdn.net/u013926582/article/details/24442583 和 http://www.arduino.cn/thread-2485-1-1.html 都有提到。

最后使用的方法是 查看 C:\Windows\inf\setupapi.dev.log (要从下向上搜索,最近安装的错误在文件尾)

发现下面的字样

dvi: {DIF_INSTALLDEVICEFILES} 17:08:34.464
dvi: Class installer: Enter 17:08:34.465
dvi: Class installer: Exit
dvi: Default installer: Enter 17:08:34.465
dvi: {Install FILES}mdmcpq.inf
inf: Opened PNF: ‘c:\windows\system32\driverstore\filerepository\arduino.inf_amd64_neutral_844213a156728dfe\arduino.inf’ ([strings])
inf: Opened PNF: ‘C:\Windows\INF\mdmcpq.inf’ ([strings])
inf: Opened PNF: ‘C:\Windows\INF\usb.inf’ ([strings.0804])
inf: {Install Inf Section [DriverInstall]}
inf: CopyFiles=FakeModemCopyFileSection (arduino.inf line 128)
cpy: Open PnpLockdownPolicy: Err=2. This is OK. Use LockDownPolicyDefault
flq: QueueSingleCopy…
flq: Inf : ‘c:\windows\system32\driverstore\filerepository\arduino.inf_amd64_neutral_844213a156728dfe\arduino.inf’
! flq: Missing SourceDisksFiles/SourceDisksNames information from INF.
! flq: Default INBOX source locations pulled from pre-built DrvIndex
flq: SourceRootPath: ‘C:\Windows\System32\DriverStore\FileRepository\mdmcpq.inf_amd64_neutral_b53453733bd795bc’
flq: {FILE_QUEUE_COPY}
flq: CopyStyle – 0x00002000
flq: {FILE_QUEUE_COPY}
flq: CopyStyle – 0x00002000
flq: SourceRootPath – ‘C:\Windows\System32\DriverStore\FileRepository\mdmcpq.inf_amd64_neutral_b53453733bd795bc’
flq: SourcePath – ‘\’
flq: SourceFilename – ‘usbser.sys’
flq: TargetDirectory- ‘C:\Windows\system32\DRIVERS’
flq: TargetFilename – ‘usbser.sys’
flq: {FILE_QUEUE_COPY exit(0x00000000)}
flq: {FILE_QUEUE_COPY exit(0x00000000)}
inf: {Install Inf Section [DriverInstall] exit (0x00000000)}
dvi: Processing co-installer registration section [DriverInstall.CoInstallers].
inf: {Install Inf Section [DriverInstall.CoInstallers]}
inf: {Install Inf Section [DriverInstall.CoInstallers] exit (0x00000000)}
dvi: Co-installers registered.
dvi: {Install INTERFACES}
dvi: Installing section [DriverInstall.Interfaces]
dvi: {Install INTERFACES exit 00000000}
dvi: {Install FILES exit (0x00000000)}
dvi: Default installer: Exit
dvi: {DIF_INSTALLDEVICEFILES – exit(0x00000000)} 17:08:34.479
ndv: Pruning file queue…
dvi: {_SCAN_FILE_QUEUE}
flq: ScanQ flags=620
flq: SPQ_SCAN_PRUNE_COPY_QUEUE
flq: SPQ_SCAN_FILE_COMPARISON
flq: SPQ_SCAN_ACTIVATE_DRP
flq: ScanQ number of copy nodes=1
! sig: GetNameSDInfo
! sig: Error 0: The operation completed successfully.
flq: ScanQ action=200 DoPruning=32
flq: ScanQ end Validity flags=620 CopyNodes=1
dvi: {_SCAN_FILE_QUEUE exit(0, 0x00000000)}
ndv: Committing file queue…
flq: {_commit_file_queue}
flq: CommitQ DelNodes=0 RenNodes=0 CopyNodes=1
flq: {SPFILENOTIFY_STARTQUEUE}
flq: {SPFILENOTIFY_STARTQUEUE – exit(0x00000001)}
flq: {_commit_copy_subqueue}
flq: subqueue count=1
flq: {SPFILENOTIFY_STARTSUBQUEUE}
flq: {SPFILENOTIFY_STARTSUBQUEUE – exit(0x00000001)}
flq: source media:
flq: SourcePath – [C:\Windows\System32\DriverStore\FileRepository\mdmcpq.inf_amd64_neutral_b53453733bd795bc]
flq: SourceFile – [usbser.sys]
flq: Flags – 0x00000000
flq: {SPFQNOTIFY_NEEDMEDIA}
flq: {SPFILENOTIFY_NEEDMEDIA}
flq: {SPFILENOTIFY_NEEDMEDIA – exit(0x00000000)}
flq: {SPFQNOTIFY_NEEDMEDIA – returned 0x00000000}
!!! flq: source media: SPFQOPERATION_ABORT.
!!! flq: Error 2: The system cannot find the file specified.
flq: {_commit_copy_subqueue exit(0x00000002)}
!!! flq: FileQueueCommit aborting!
!!! flq: Error 2: The system cannot find the file specified.
flq: {SPFILENOTIFY_ENDQUEUE}
flq: {SPFILENOTIFY_ENDQUEUE – exit(0x00000001)}
flq: {_commit_file_queue exit(0x00000002)}
ndv: Device install status=0x00000002
ndv: Performing device install final cleanup…
! ndv: Queueing up error report since device installation failed…
ndv: {Core Device Install – exit(0x00000002)} 17:08:34.493
dvi: {DIF_DESTROYPRIVATEDATA} 17:08:34.494
dvi: Class installer: Enter 17:08:34.494
dvi: Class installer: Exit

错误是说找不到 C:\Windows\System32\DriverStore\FileRepository\mdmcpq.inf_amd64_neutral_b53453733bd795bc(特别注意,不同系统,这个名字会不同,需要根据你当前系统中的错误来确定) 我进去 C:\Windows\System32\DriverStore\FileRepository\ 一看确实没有。

又在网上搜索了一下,在 http://www.arduino.cn/thread-2350-1-1.html 这里给出来两个64位的。

下载之后,首先将 UsbSer.sys 放到 Windows\System32\Drivers 下面,然后在 C:\Windows\System32\DriverStore\FileRepository\ 下面创建一个 mdmcpq.inf_amd64_neutral_b53453733bd795bc 目录把mdmcpq.zip中的全部内容放进去。

卸载之前安装的驱动,然后重新安装一次即可。

usbser

mdmcpq.inf

如果有遇到同样问题的朋友可以试试。

===========================================================================
2月16日 如果始终无法完成安装,建议检查一下你是否将设备插在 USB 3.0 的Port上。某些USB 3.0的 Controller 对于一些低速设备存在兼容性问题。如果是的话,请更换到 USB 2.0 的端口上再进行上述实验。

4月4日 在FileRepository 目录下创建目录的过程中,可能遇到“需要权限来执行此操作”的问题。解决方法是:

1.确保你的杀毒软件关闭了(比如,瑞星之类的你可以考虑直接卸载)
2.在FileRepository 目录上点鼠标右键,弹出的菜单上切换到安全页面。然后在上面的组或用户名栏目上检查是否有你当前账户。比如:开机登录的用户名是 player ,而这里没有。那么按下编辑键,在新弹出页面上用添加按钮,然后输入 player ,按下检查名称按钮就添加进去了。特别注意:其中的 SYSTEM 用户名,并非 Administrator,如果你是 Administrator 登录进来,那么需要手工添加一下。

Arduino 显示奶瓶温度

有个网友提出一个需求“做一个能够显示温度的奶瓶。奶瓶泡好奶后放在底座上,可直接给婴儿喝的绿灯亮,过烫红灯亮,过凉蓝灯亮。”这是他的一个作业,然后我就帮着做了一下。整体思路就是直接使用上一次做的那个红外温度计的方案,我想这样的方案恐怕在网上都是独一无二的吧。

使用到的基本元件有:Arduino Uno + LED + 红外温度传感器(上面有一个激光头用于瞄准)

使用红外温度传感器能够获得下列优点:

1.无需接触,不必考虑卫生的问题
2.准确度在可以接受的范围内
3.速度快,对准之后马上显示
4.兼容性好,各种型号的奶瓶都可以

缺点:

1.贵
2.有人说无法精确得知瓶中奶的温度,测量的是奶瓶温度而已

milkb2

milkb1

#include <Arduino.h>
#include <Wire.h>
#include "TN901.h"  
TN901 tn;          
int RedLed=10;  //Red Led Pin10
int GreenLed=7;  //Green Led Pin10
int BigRedLed=4;  //Big Led Pin4
void setup()
{
    Serial.begin(9600);
    tn.Init(13,12,11);  //初 始 化 data clk ack
    pinMode(RedLed,OUTPUT);
    digitalWrite(RedLed,LOW);
    pinMode(GreenLed,OUTPUT);
    digitalWrite(GreenLed,LOW);    
    pinMode(BigRedLed,OUTPUT);
    digitalWrite(BigRedLed,LOW);      
}
void loop()
{
   tn.Read();
   SerialValue();
   delay(2000);
}

void SerialValue()
{
   String s;
   s=">O"+String(tn.OT, DEC)+"E"+String(tn.ET,DEC)+"<";
   Serial.println(s);
   if (tn.OT>5000) { // Temp too high
         digitalWrite(RedLed,HIGH);
         digitalWrite(GreenLed,LOW);
         digitalWrite(BigRedLed,HIGH);        
   }
   else {
     if (tn.OT>2100) {// Temp is OK
         digitalWrite(RedLed,LOW);
         digitalWrite(GreenLed,HIGH);
         digitalWrite(BigRedLed,HIGH);        
         }
    else { //Temp is too Low      
         digitalWrite(RedLed,HIGH);
         digitalWrite(GreenLed,HIGH);
         digitalWrite(BigRedLed,LOW);
     
    }
   }  
}

 

设计上我们使用了2个LED,一个是小LED双色红色和绿色,一个是单独的大个红色的(手边只有这两种LED,更好的设计应该换个颜色的LED便于区分)。温度传感器是红外温度传感器。

温度区间:

>50 小LED的红色亮起 表示温度过高

50-20 小LED的绿色亮起 表示温度适合

<20 大红色LED亮起 表示温度过低 工作时的视频 首先展示一下电路,然后我们有一瓶大瓶冷水;放在上面的时候温度过低,大红色 LED亮起;然后我们换成奶瓶,其中是热水,温度过高,小红色LED亮起;然后我们将大瓶子的水兑到小瓶子中,温度会变得适中,于是小LED绿色会亮起。

这个方案对于作业来说应该是足够了。就是这样。

Step to UEFI (34) —– FindFile2 查找特定文件

前面介绍了如何枚举全部文件,这里介绍一下如何枚举特定的问题。比如,用 “M*.*” 匹配全部 M开头的文件。

参考 touch 命令的Source Code 很快有了方案,使用:ShellOpenFileMetaArg

对应的头文件在 \ShellPkg\Include\Library\ShellLib.h

/**
  Opens a group of files based on a path.

  This function uses the Arg to open all the matching files. Each matched
  file has a SHELL_FILE_ARG structure to record the file information. These
  structures are placed on the list ListHead. Users can get the SHELL_FILE_ARG
  structures from ListHead to access each file. This function supports wildcards
  and will process '?' and '*' as such.  The list must be freed with a call to
  ShellCloseFileMetaArg().

  If you are NOT appending to an existing list *ListHead must be NULL.  If
  *ListHead is NULL then it must be callee freed.

  @param[in] Arg                 The pointer to path string.
  @param[in] OpenMode            Mode to open files with.
  @param[in, out] ListHead       Head of linked list of results.

  @retval EFI_SUCCESS           The operation was sucessful and the list head
                                contains the list of opened files.
  @retval != EFI_SUCCESS        The operation failed.

  @sa InternalShellConvertFileListType
**/
EFI_STATUS
EFIAPI
ShellOpenFileMetaArg (
  IN CHAR16                     *Arg,
  IN UINT64                     OpenMode,
  IN OUT EFI_SHELL_FILE_INFO    **ListHead
  );

 

从介绍上来看,最主要的参数有2个:Arg输入要查找的路径,可以使用通配符。ListHead 返回结果。结果实际上是两部分一部分是 SHELL_FILE_ARG 结构体,另外一部分是 EFI_SHELL_FILE_INFO 结构体。前者只有一个,后者有一个或者很多个。他们使用链表结构联系在一起。上面这样的结构体感觉上很奇怪,不过确实是这样的。可以在Touch的 Source Code中看到。他使用 GetFirstNode 来跳过第一个不需要的结构体。这个函数可以在 \MdePkg\Library\BaseLib\LinkedList.c 里面看到

/**
  Retrieves the first node of a doubly-linked list.

  Returns the first node of a doubly-linked list.  List must have been 
  initialized with INTIALIZE_LIST_HEAD_VARIABLE() or InitializeListHead().
  If List is empty, then List is returned.

  If List is NULL, then ASSERT().
  If List was not initialized with INTIALIZE_LIST_HEAD_VARIABLE() or 
  InitializeListHead(), then ASSERT().
  If PcdMaximumLinkedListLenth is not zero, and the number of nodes
  in List, including the List node, is greater than or equal to
  PcdMaximumLinkedListLength, then ASSERT().

  @param  List  A pointer to the head node of a doubly-linked list.

  @return The first node of a doubly-linked list.
  @retval NULL  The list is empty.

**/
LIST_ENTRY *
EFIAPI
GetFirstNode (
  IN      CONST LIST_ENTRY          *List
  )
{
  //
  // ASSERT List not too long
  //
  ASSERT (InternalBaseLibIsNodeInList (List, List, FALSE));

  return List->ForwardLink;
}

 

最终编写程序如下

#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;

void PrintShellFileInfo(EFI_SHELL_FILE_INFO     *ShellFileInfo)
{
  //Print(L"Status [%d]\n",ShellFileInfo-> Status);
  Print(L"FullName [%s]\n",ShellFileInfo-> FullName);
  //Print(L"FileName [%s]\n",ShellFileInfo-> FileName);
  //Print(L"Handle [%d]\n",ShellFileInfo->Handle);  
}

int
EFIAPI
main (                                         
  IN int Argc,
  IN char **Argv
  )
{
  //EFI_FILE_HANDLE   DirHandle;
  RETURN_STATUS     Status;
  EFI_SHELL_FILE_INFO *FileList=NULL;
  EFI_SHELL_FILE_INFO *Node;
  
  Status = ShellOpenFileMetaArg(L"fsnt0:\\a*.*", EFI_FILE_MODE_READ, &FileList);
  if(Status != RETURN_SUCCESS) {
        Print(L"OpenFile failed!\n");
		return EFI_SUCCESS;
  }							   
  
  //Print(L"Signature [%X]\n",((SHELL_FILE_ARG *)&FileList)->Signature);  
  //Print(L"Status []\n",((SHELL_FILE_ARG *)&FileList)->Status);  
  //Print(L"ParentName [%s]\n",((SHELL_FILE_ARG *)&FileList)->ParentName);  
  //Print(L"FullName [%s]\n",((SHELL_FILE_ARG *)&FileList)->FullName);  
  //Print(L"FileName [%s]\n",((SHELL_FILE_ARG *)&FileList)->FileName);  

  
  // check that we have at least 1 file
  //
  if (FileList == NULL || IsListEmpty(&FileList->Link)) {
                Print(L"No Files Found!\n");
  } else {
		//
		// loop through the list and make sure we are not aborting...
		//
		for ( Node = (EFI_SHELL_FILE_INFO*)GetFirstNode(&FileList->Link)
			; !IsNull(&FileList->Link, &Node->Link) && !ShellGetExecutionBreakFlag()
			; Node = (EFI_SHELL_FILE_INFO*)GetNextNode(&FileList->Link, &Node->Link)){
					PrintShellFileInfo(Node);			   
				//
				// make sure the file opened ok
				//
				if (EFI_ERROR(Node->Status)){
					Print(L"OpenFile Error!\n");
				}

        }
    }
		  
   //
   // Free the fileList
   //
   if (FileList != NULL && !IsListEmpty(&FileList->Link)) {
       Status = ShellCloseFileMetaArg(&FileList);
   }
   FileList = NULL;
		
  return EFI_SUCCESS;
}

 

运行结果

FindFIle2

可以看出能够正常查找到我们需要的 fsnt0:x下面a开头的文件。

代码下载

FindFile2

===============================================================================
最后有一个问题:
前面说过,ShellOpenFileMetaArg 输出的结果是2部分组成的,我的程序只是输出了后面的那个结构体,那么前面的那个结构体呢?

  Status = ShellOpenFileMetaArg(L"fsnt0:\\a*.*", EFI_FILE_MODE_READ, &FileList);
  if(Status != RETURN_SUCCESS) {
        Print(L"OpenFile failed!\n");
		return EFI_SUCCESS;
  }							   
  
  Print(L"Signature [%X]\n",((SHELL_FILE_ARG *)&FileList)->Signature);  
  Print(L"Status []\n",((SHELL_FILE_ARG *)&FileList)->Status);  
  Print(L"ParentName [%s]\n",((SHELL_FILE_ARG *)&FileList)->ParentName);  
  Print(L"FullName [%s]\n",((SHELL_FILE_ARG *)&FileList)->FullName);  
  Print(L"FileName [%s]\n",((SHELL_FILE_ARG *)&FileList)->FileName); 

 

结果出来之后非常奇怪的是 Signature 的输出并非固定的数值,而根据 EfiShellEnvironment2.h 来看,这个似乎应该是一个固定的值。

不知道是我理解有偏差还是输出方法不对。有懂的朋友请指教一下。谢谢!

窗花

前几天看《小说月报》,一篇讲述的是研究生毕业的漂亮女硕士,为了完成女导师的愿望,毅然决然选择从政的故事。只不过这是主旋律小说,必须有光明的尾巴:一直期望她能成为自己“李万姬”的老领导患了癌症,向革命老前辈们报到前说出了所有的事情,于是全部矛盾迎刃而解。最后女主角镀金完成,换了岗位,抱得男朋友满载而归。比较有意思的是故事中有一个情节,讲的是他们那里支柱企业是无纺布生产厂,受到经济危机影响,贷款出了问题,然后女主角想办法筹集资金,拖欠了一下当地干部和教师的工资,又下达集资的命令。看到资金的来源,我觉得这篇小说还真是写实。确实,对于教师这个职业,没有好处的很多时候不会想起来你是公务员,但是要是有需要体现精神的时候又会被推到前面。

又想起来很久之前的一个科幻小说,讲述人类发送太阳能电池到太空作为新能源,但是太空中有尘埃,每隔一段就需要清理。发送博士之类风险太大薪水又不能太低,最后作为特产,农民工就被送到太空,从事这份高工资的工作,也算是人上人了。小说还真是传承社会精神面貌的镜子。回头我找本书好好研究下宋朝的生活,顺便批判一下封建社会的腐朽的生活方式。

我这里说的窗花并非那种传统手工图样,而是凝结在窗户上的水汽形成的美丽图样。

0f53c8d303a1add716cbc1cbef375ee8

我的一位同学,是小学老师,前一段她每天早晨都会在朋友圈中晒出办公室窗花的照片。听起来很浪漫,现实中意味着寒冷—-办公室没有暖气和空调。听起来难以置信,不过据她所说就是这样。此外,因为种种安全上的考量严禁各种非生物能源转换,意思是除了蹦跳多穿衣服多吃东西之外严禁使用电器。于是她便只能望着美丽的窗花偶发感慨聊以自慰了。

以前我在的公司也有过这样的事情:大冬天为了省电不开空调,HW工程师冻的手都没有办法握稳电烙铁。具体是这样的,冬季是我国江南地区传统的用电高峰,企业在用电上会有一些限制,于是老板就在空调上打起来主意。最大的问题是老板的房间是在整栋楼的中间,好比上下各有一层脂肪,大约是感受不到多少寒冷,而我们研发非常不幸的处在顶楼,不开空调很冷啊。我们找了一楼的总务很多次,总务总是指着墙上的一直温度计辩解还没有降低到18度,由拒绝开,我甚至一直在怀疑,他那只温度计是特殊定制的,显示值会比正常的温度高那么两三度。再后来终于开放中央空调,但是还想了一个非常奇特的规则:整点的时候供电10分钟。意思是,比如九点到九点十分,中央空调是有电的,但是需要手工用遥控器打开之。于是,每逢快到整点,研发人员就像神经病一样相互提醒马上准备开空调,能暖和一段。偶然忘记了大家更是垂足顿胸。

时间一长,大家都受不鸟了。就开始研究解决方法了。做HW的工程师夏天的时候可以做几只风扇并联起来,但是冬天的时候没有太合适的工具取暖,热风枪吹起来只有外焦里生的效果。电烙铁的加热区域也太小,没办法满足四肢的需要…….通常 HW 搞不定的就要靠SW了。于是,我旁边的妹子提出:太TMD冷了,自己买个暖风机,然后我也附议了一下,一起买吧。忽然想起来那次似乎是我第一次在京东上购物。根据自己的体积,我挑选了一个2KW的暖风机,旁边的妹子挑选了一个2.5KW的。头天下午的订单,第二天就送货上门签收交易完成,那也是我第一次被京东的配送速度震撼到。用起来感觉不错,是风暖型的,中午熄灯之后吹上去暖洋洋的,我恨不得搬着一张床钻到桌子下面去。等我们用了一段,看看没啥动静,办公室的人也都一窝蜂的去买,到了最后几乎是人手一只。连研发的老大也悄悄买了一个低调的塞在座位下面:高调的理念通常只适用在别人身上,真正到了自己这边舒服一些比什么都强。大约也是到了这个岁数所谓天降大人于私人也不那么重要。没有大志终究比大痔或者冻疮强。

再后来,总务想管来着,羞羞答答的提出了除了费电之外的全部理由,比如:不安全,不环保等等。只是研发没人鸟。想必总务也是有所顾忌的,一方面大冬天不给开空调本来就不占理;另一方面也许是实在讲不过研发的人吧。再后来也就不了了之,我们吹着暖风迎来了春天………..

事后,我曾经认真的总结,这个事情之所以能成,很大程度上是总务对我们有所顾忌,也就睁一只眼闭一只眼了;更高一点层次来说,研发人员算是稀缺,上面也不愿意太过分。再后面发生太过分的事情,研发3个月走了一半的。

从另外一个角度来说,我同学那样的小学老师,生活是很稳定的,一辈子不用担心你会被开除或者因为某些变故失去工作,而这种稳定付出的代价就是自由。