Arduino 心率带测试

目前市面上能够测量心率的设备很多。有腕带腕表式的,也有夹在耳朵或者手指末端的。从准确性上来说,腕带式的容易松动因此没有胸带式的准确。同时,胸带式的对于运动统计来说也是最好的选择。

前一段入手了三根心率带和一个接收模块。其中的心率带是带有编码的,因此在接收端可以很容易的区分数据来源。当然,与之对应的还有不带编码的心率带,无法在多个的情况下使用。

先说说心率带模块:

  1. 内部使用 CR2032 纽扣电池,据说每天工作1小时可以撑9个月;
  2. 发送频率为 2.4Ghz;
  3. 平时处于睡眠模式,戴上之后才开始发送数据
  4. 发射数据每次 4 Bytes,3 Bytes ID+ 1 Byte 心率。

接收模块:

  1. 模块三个引脚,GND  VCC (特别强调是 3.3V供电) TX 。使用串口通讯。波特率 9600 bps。比如:DD 20 03 04 50(个人感觉有点低,设想如果很多根在同一个空间内使用不知道会有什么问题);
  2. 上面有一个LED,当收到有效数据后会闪动一次;
  3. 板子上的孔间距是 2.0mm 不是 2.54,使用杜邦线会很别扭,我直接焊接上导线来使用;

下面是一个完整的例子,使用 Arduino Leonardo 接收数据,将收到的数据总结为 A + 每分钟心跳数(bpm),或者  B +每分钟心跳数上传到USB串口,然后在上位机上显示出来。试验中使用的心率带心率带 A  ID :22 05 28   心率带 B  ID :22 06 58

  1. Arduino代码
void setup() {
  Serial.begin(9600);
  Serial1.begin(9600);
}

boolean status1=false;
boolean status2=false;
boolean status3=false;
byte    addr[3];
byte    pos=0;
char    outadd;
void loop() {
  byte c;
  while (Serial1.available()) 
   {
      c=Serial1.read()&0xFF;
           Serial.write(c);
      if ((status1==false)&&(c==0xdd)) {
            status1=true;
            status2=true;
        //         Serial.print("st1");
        }
      else if (status2) {
            addr[pos]=c;
            pos++;
            if (pos==3) {
                 status2=false;
                 status3=true;   
              }
          //  Serial.print("st2");  
        }
      else if (status3) {
              //Serial.print("st3");
              if ((addr[0]==0x22)&&(addr[1]==0x05)&&(addr[2]==0x28)) {
                 Serial.write('A');
                 Serial.write(c);
                 Serial.println("");
               }
              else
              if ((addr[0]==0x22)&&(addr[1]==0x06)&&(addr[2]==0x58)) {
                 Serial.write('B');
                 Serial.write(c);
                 Serial.println("");
               }
             status1=false;
             status2=false;
             status3=false;
             pos=0;  
     }   
   } //while

  • C# 编写上位机程序,在界面上绘制当前心率曲线

完整代码

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;

namespace WindowsFormsApplication9
{
    public partial class Form1 : Form
    {
        private Queue<double> dataQueue = new Queue<double>(100);
        private Queue<double> dataQueue2 = new Queue<double>(100);

        private int num = 5;//每次删除增加几个点
        private int heartA;
        private int heartB;
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            //textBox1.AppendText("start");
            Form2 f2 = new Form2();
            f2.ShowDialog();
            serialPort1.PortName = "COM" + f2.SelectedPort.ToString();
           // MessageBox.Show(f2.SelectedPort.ToString());
            button3.Enabled = true;
            button4.Enabled = true;
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            //设置窗体为无边框样式
            this.FormBorderStyle = FormBorderStyle.None;
            //最大化窗体    
            this.WindowState = FormWindowState.Maximized;     

            InitChart();
            chart1.Width = this.Width - 20;
            chart1.Height = this.Height - 100;
            button3.Enabled = false;
            button4.Enabled = false;

            //如果没有这句话,串口收到的只有0-128 的 ASCII
            serialPort1.Encoding = System.Text.Encoding.GetEncoding(28591);
            serialPort1.DtrEnable = true;
            serialPort1.RtsEnable = true;
        }

        /// <summary>
        /// 初始化图表
        /// </summary>
        private void InitChart()
        {
            //定义图表区域
            this.chart1.ChartAreas.Clear();
            ChartArea chartArea1 = new ChartArea("C1");
            this.chart1.ChartAreas.Add(chartArea1);
            //定义存储和显示点的容器
            this.chart1.Series.Clear();
            Series series1 = new Series("S1");
            series1.ChartArea = "C1";
            this.chart1.Series.Add(series1);

            Series series2 = new Series("S2");
            this.chart1.Series.Add(series2);
            //设置图表显示样式
            this.chart1.ChartAreas[0].AxisY.Minimum = 50;
            this.chart1.ChartAreas[0].AxisY.Maximum = 170;
            this.chart1.ChartAreas[0].AxisY.Interval = 10;
            this.chart1.ChartAreas[0].AxisX.Interval = 5;
            this.chart1.ChartAreas[0].AxisX.MajorGrid.LineColor = System.Drawing.Color.Silver;
            this.chart1.ChartAreas[0].AxisY.MajorGrid.LineColor = System.Drawing.Color.Silver;
            //设置标题
            this.chart1.Titles.Clear();
            this.chart1.Titles.Add("Red");
            this.chart1.Titles[0].Text = "Heart";
            this.chart1.Titles[0].Font = new System.Drawing.Font("Microsoft Sans Serif", 12F);
            this.chart1.Titles[0].ForeColor = Color.Red;
            //设置图表显示样式
            this.chart1.Series[0].Color = Color.Red;
            this.chart1.Series[0].ChartType = SeriesChartType.Line;
            this.chart1.Series[0].Points.Clear();

            this.chart1.Series[1].Color = Color.Blue;
            this.chart1.Series[1].ChartType = SeriesChartType.Line;

        }

        private void button2_Click(object sender, EventArgs e)
        {
            Application.Exit();
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            UpdateQueueValue();
            this.chart1.Series[0].Points.Clear();
            this.chart1.Series[1].Points.Clear();
            for (int i = 0; i < dataQueue.Count; i++)
            {
                this.chart1.Series[0].Points.AddXY((i + 1), dataQueue.ElementAt(i));
                this.chart1.Series[1].Points.AddXY((i + 1), dataQueue2.ElementAt(i));
            }
        }

        //更新队列中的值
        private void UpdateQueueValue()
        {
            int heart=0;
            if (dataQueue.Count >= 100)
            {
                //先出列
                for (int i = 0; i < num; i++)
                {
                    dataQueue.Dequeue();
                    dataQueue2.Dequeue();
                }
            }

                //Random r = new Random();
                for (int i = 0; i < num; i++)
                {
                    //heart = r.Next(50, 170);
                    dataQueue.Enqueue(heartA);
                    //heart = r.Next(50, 170);
                    dataQueue2.Enqueue(heartB);
                }

                chart1.Titles[0].Text = "Heart"+ heart.ToString();

        }

        private void button3_Click(object sender, EventArgs e)
        {
            this.serialPort1.Open();
            this.timer1.Enabled = true;
            this.timer2.Enabled = true;
        }

        private void button4_Click(object sender, EventArgs e)
        {
            this.timer1.Enabled = false;
            this.timer2.Enabled = false;
            serialPort1.Close();
            this.timer1.Stop();
            this.timer2.Stop();
        }

        private void serialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
        {

        }

        private void timer2_Tick(object sender, EventArgs e)
        {
            if (serialPort1.BytesToRead!=0) {
                
                String s=serialPort1.ReadLine();
                if (s.Length>=2) { 
                if (s[0] == 'A') {
                        heartA = s[1]; // Convert.ToInt32(s[1]);
                    textBox1.AppendText(heartA.ToString());
                }
                else
                    if (s[0] == 'B')
                      {
                        heartB = s[1];
                        //textBox1.AppendText(heartB.ToString("X2"));
                }
                }
            }
        }
    }
}

运行结果:

工作的视频在  https://zhuanlan.zhihu.com/p/56291800

Step to UEFI (181)GetTime 研究

最近在编写一个需要随机生成数值的代码,使用之前的 rand 函数【参考1】发现每次生成的随机数是相同的,忽然意识到这是因为代码里面的随机种子是固定值导致的,如果使用当前时间作为随机种子那么每次生成的数值将会是不同的。

可以使用 Runtime Service 中的 GetTime 来取得时间作为种子,返回的时间格式如下:

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

根据上面的结构编写测试代码如下:

/** @file
    A simple, basic, EDK II native, "hello" application to verify that
    we can build applications without LibC.

    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  <Library/UefiBootServicesTableLib.h> //global gST gBS gImageHandle
#include  <Library/ShellLib.h>

extern EFI_RUNTIME_SERVICES      *gRT;

EFI_TIME        ET;

/***
  Print a welcoming message.

  Establishes the main structure of the application.

  @retval  0         The application exited normally.
  @retval  Other     An error occurred.
***/
INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  )
{
        gRT->GetTime(&ET,NULL);
        
        Print(L"Hour  [%d]\n",ET.Hour);
        Print(L"Minute[%d]\n",ET.Minute);
        Print(L"Second[%d]\n",ET.Second);
        Print(L"Nano  [%d]\n",ET.Nanosecond);
       
        return(0);
}

在NT32 虚拟机中运行结果如下:

但是实体机上(KBL-R)跑出来的结果显示 Nanosecond 始终为 0。于是开始研究这个代码。

首先,在 NT32 环境下,GetTime 函数具体实现在  \Nt32Pkg\RealTimeClockRuntimeDxe\RealTimeClock.c 中。

EFI_STATUS
EFIAPI
InitializeRealTimeClock (
  IN EFI_HANDLE                            ImageHandle,
  IN EFI_SYSTEM_TABLE                      *SystemTable
  )
  SystemTable->RuntimeServices->GetTime       = WinNtGetTime;
  SystemTable->RuntimeServices->SetTime       = WinNtSetTime;

具体实现

EFI_STATUS
EFIAPI
WinNtGetTime (
  OUT EFI_TIME                                 *Time,
  OUT EFI_TIME_CAPABILITIES                    *Capabilities OPTIONAL
  )
/*++

Routine Description:
  Service routine for RealTimeClockInstance->GetTime 

Arguments:

  Time          - A pointer to storage that will receive a snapshot of the current time.

  Capabilities  - A pointer to storage that will receive the capabilities of the real time clock
                  in the platform. This includes the real time clock's resolution and accuracy.  
                  All reported device capabilities are rounded up.  This is an OPTIONAL argument.

Returns:

  EFI_SUCEESS   - The underlying GetSystemTime call occurred and returned
                  Note that in the NT32 emulation, the GetSystemTime call has no return value
                  thus you will always receive a EFI_SUCCESS on this.

--*/
// TODO:    EFI_INVALID_PARAMETER - add return value to function comment
{
  SYSTEMTIME            SystemTime;
  TIME_ZONE_INFORMATION TimeZone;

  //
  // Check parameter for null pointer
  //
  if (Time == NULL) {
    return EFI_INVALID_PARAMETER;

  }

  gWinNt->GetLocalTime (&SystemTime);
  gWinNt->GetTimeZoneInformation (&TimeZone);

  Time->Year        = (UINT16) SystemTime.wYear;
  Time->Month       = (UINT8) SystemTime.wMonth;
  Time->Day         = (UINT8) SystemTime.wDay;
  Time->Hour        = (UINT8) SystemTime.wHour;
  Time->Minute      = (UINT8) SystemTime.wMinute;
  Time->Second      = (UINT8) SystemTime.wSecond;
  Time->Nanosecond  = (UINT32) (SystemTime.wMilliseconds * 1000000);
  Time->TimeZone    = (INT16) TimeZone.Bias;

  if (Capabilities != NULL) {
    Capabilities->Resolution  = 1;
    Capabilities->Accuracy    = 50000000;
    Capabilities->SetsToZero  = FALSE;
  }

  Time->Daylight = 0;
  if (TimeZone.StandardDate.wMonth) {
    Time->Daylight = (UINT8) TimeZone.StandardDate.wMonth;
  }

  return EFI_SUCCESS;
}

从上面可以看到  Time->Nanosecond  来自  (SystemTime.wMilliseconds * 1000000)。

接下来,在实体机BIOS代码中检查,发现赋值函数在 \PcAtChipsetPkg\PcatRealTimeClockRuntimeDxe\PcRtc.c 文件中:

/**
  Converts time read from RTC to EFI_TIME format defined by UEFI spec.

  This function converts raw time data read from RTC to the EFI_TIME format
  defined by UEFI spec.
  If data mode of RTC is BCD, then converts it to decimal,
  If RTC is in 12-hour format, then converts it to 24-hour format.

  @param   Time       On input, the time data read from RTC to convert
                      On output, the time converted to UEFI format
  @param   RegisterB  Value of Register B of RTC, indicating data mode
                      and hour format.

  @retval  EFI_INVALID_PARAMETER  Parameters passed in are invalid.
  @retval  EFI_SUCCESS            Convert RTC time to EFI time successfully.

**/
EFI_STATUS
ConvertRtcTimeToEfiTime (
  IN OUT EFI_TIME        *Time,
  IN     RTC_REGISTER_B  RegisterB
  )
{
  BOOLEAN IsPM;
  UINT8   Century;

  if ((Time->Hour & 0x80) != 0) {
    IsPM = TRUE;
  } else {
    IsPM = FALSE;
  }

  Time->Hour = (UINT8) (Time->Hour & 0x7f);

  if (RegisterB.Bits.Dm == 0) {
    Time->Year    = CheckAndConvertBcd8ToDecimal8 ((UINT8) Time->Year);
    Time->Month   = CheckAndConvertBcd8ToDecimal8 (Time->Month);
    Time->Day     = CheckAndConvertBcd8ToDecimal8 (Time->Day);
    Time->Hour    = CheckAndConvertBcd8ToDecimal8 (Time->Hour);
    Time->Minute  = CheckAndConvertBcd8ToDecimal8 (Time->Minute);
    Time->Second  = CheckAndConvertBcd8ToDecimal8 (Time->Second);
  }

  if (Time->Year == 0xff || Time->Month == 0xff || Time->Day == 0xff ||
      Time->Hour == 0xff || Time->Minute == 0xff || Time->Second == 0xff) {
    return EFI_INVALID_PARAMETER;
  }

  //
  // For minimal/maximum year range [1970, 2069],
  //   Century is 19 if RTC year >= 70,
  //   Century is 20 otherwise.
  //
  Century = (UINT8) (PcdGet16 (PcdMinimalValidYear) / 100);
  if (Time->Year < PcdGet16 (PcdMinimalValidYear) % 100) {
    Century++;
  }
  Time->Year = (UINT16) (Century * 100 + Time->Year);

  //
  // If time is in 12 hour format, convert it to 24 hour format
  //
  if (RegisterB.Bits.Mil == 0) {
    if (IsPM && Time->Hour < 12) {
      Time->Hour = (UINT8) (Time->Hour + 12);
    }

    if (!IsPM && Time->Hour == 12) {
      Time->Hour = 0;
    }
  }

  Time->Nanosecond  = 0;

  return EFI_SUCCESS;
}

其中给定的Time->Nanosecond  始终为 0. 这就是为什么我们在实体机看到0的原因。

完整的代码如下:

最终,我使用 (ET.Hour* ET.Minute* ET.Second) 作为种子。当调用间隔时间足够长(至少大于1秒),这种方式能够正常工作并且足够简单。如果你对安全性有特别要求,建议使用 RDRand 来取得随机数。

参考:

1. http://www.lab-z.com/clibrand/ CLIB:RAND 随机数生成

Step to UEFI (183)SetJump() 和LongJump()

单纯的函数调用从动作上来说,Call 指令可以看作保存寄存器压栈外加 JMP 的过程(复杂的说调用 Call 会有有实模式,保护模式, Near/Far,gated等等差别,在 IA32 手册上描述足有8页之多)。因此,可以使用压栈保存寄存器外加一个跳转来实现。在 UEFI 中可以通过SetJump() 和 LongJump()函数组合来实现。

在 \MdePkg\Include\Library\BaseLib.h 可以看到下面的定义。

/**
  Saves the current CPU context that can be restored with a call to LongJump()
  and returns 0.

  Saves the current CPU context in the buffer specified by JumpBuffer and
  returns 0. The initial call to SetJump() must always return 0. Subsequent
  calls to LongJump() cause a non-zero value to be returned by SetJump().

  If JumpBuffer is NULL, then ASSERT().
  For Itanium processors, if JumpBuffer is not aligned on a 16-byte boundary, then ASSERT().

  NOTE: The structure BASE_LIBRARY_JUMP_BUFFER is CPU architecture specific.
  The same structure must never be used for more than one CPU architecture context.
  For example, a BASE_LIBRARY_JUMP_BUFFER allocated by an IA-32 module must never be used from an x64 module.
  SetJump()/LongJump() is not currently supported for the EBC processor type.

  @param  JumpBuffer  A pointer to CPU context buffer.

  @retval 0 Indicates a return from SetJump().

**/
RETURNS_TWICE
UINTN
EFIAPI
SetJump (
  OUT     BASE_LIBRARY_JUMP_BUFFER  *JumpBuffer
  );

从介绍上来看,SetJump能保存调用处的全部寄存器,然后返回到调用的位置。

/**
  Restores the CPU context that was saved with SetJump().

  Restores the CPU context from the buffer specified by JumpBuffer. This
  function never returns to the caller. Instead is resumes execution based on
  the state of JumpBuffer.

  If JumpBuffer is NULL, then ASSERT().
  For Itanium processors, if JumpBuffer is not aligned on a 16-byte boundary, then ASSERT().
  If Value is 0, then ASSERT().

  @param  JumpBuffer  A pointer to CPU context buffer.
  @param  Value       The value to return when the SetJump() context is
                      restored and must be non-zero.

**/
VOID
EFIAPI
LongJump (
  IN      BASE_LIBRARY_JUMP_BUFFER  *JumpBuffer,
  IN      UINTN                     Value
  );

从介绍上来看,LongJump恢复SetJump函数能保存调用处的全部寄存器,然后返回到调用SetJump函数的下一条指令的位置。

为了验证上面的说法,编写一个Application。主要代码如下:

UINTN RunMark   =       0;

void
ShowString()
{
        Print(L"www.lab-z.com [%d]\n",RunMark);
}

/***
  Print a welcoming message.

  Establishes the main structure of the application.

  @retval  0         The application exited normally.
  @retval  Other     An error occurred.
***/
INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  )
{
        BASE_LIBRARY_JUMP_BUFFER  JumpBuffer;

        SetJump(&JumpBuffer);
        RunMark++;
        ShowString();
        if (RunMark==1) {
                LongJump (&JumpBuffer, (UINTN)-1);
        }
        
        return(0);
}

运行结果如下:

第一次运行 SetJump() 会将当前寄存器存放在 JumpBuffer 中。然后 RunMark 加一,从0变成1.接着,判断这个值等于1,再执行 LongJump,执行的结果会回到  SetJump(); 的下一条指令,也就是 RunMark++。这样,RunMark 变成了2,不会运行 LongJump() 接下来顺利退出了。

最后一个问题,SetJump 和  LongJump的具体实现在哪里?我这边通过 Build 目录里面 MakeFile 内容来确认。

1.找到Application 的 MakeFile 在 \AppPkg\DEBUG_VS2015x86\X64\AppPkg\Applications\JumpCall\JumpCall\Makefile 其中有如下代码

#
# Build Macro
#
STATIC_LIBRARY_FILES =  \
    $(BIN_DIR)\MdePkg\Library\BaseDebugPrintErrorLevelLib\BaseDebugPrintErrorLevelLib\OUTPUT\BaseDebugPrintErrorLevelLib.lib \
    $(BIN_DIR)\MdePkg\Library\BasePrintLib\BasePrintLib\OUTPUT\BasePrintLib.lib \
    $(BIN_DIR)\MdePkg\Library\BasePcdLibNull\BasePcdLibNull\OUTPUT\BasePcdLibNull.lib \
$(BIN_DIR)\MdePkg\Library\BaseLib\BaseLib\OUTPUT\BaseLib.lib \

2.确定 BaseLib.Lib 的生成方式,在C:\BuildBs\201903\Build\AppPkg\DEBUG_VS2015x86\X64\MdePkg\Library\BaseLib\BaseLib\Makefile 可以看到

$(OUTPUT_DIR)\X64\SetJump.obj : $(MAKE_FILE)
$(OUTPUT_DIR)\X64\SetJump.obj : $(WORKSPACE)\MdePkg\Include\Base.h
$(OUTPUT_DIR)\X64\SetJump.obj : $(WORKSPACE)\MdePkg\Include\X64\Nasm.inc
$(OUTPUT_DIR)\X64\SetJump.obj : $(WORKSPACE)\MdePkg\Include\X64\ProcessorBind.h
$(OUTPUT_DIR)\X64\SetJump.obj : $(WORKSPACE)\MdePkg\Include\Library\PcdLib.h
$(OUTPUT_DIR)\X64\SetJump.obj : $(DEBUG_DIR)\AutoGen.h
$(OUTPUT_DIR)\X64\SetJump.obj : $(WORKSPACE)\MdePkg\Library\BaseLib\X64\SetJump.nasm
	"$(PP)" $(PP_FLAGS) $(INC) c:\buildbs\201903\MdePkg\Library\BaseLib\X64\SetJump.nasm > c:\buildbs\201903\Build\AppPkg\DEBUG_VS2015x86\X64\MdePkg\Library\BaseLib\BaseLib\OUTPUT\X64\SetJump.i
	Trim --trim-long --source-code -o c:\buildbs\201903\Build\AppPkg\DEBUG_VS2015x86\X64\MdePkg\Library\BaseLib\BaseLib\OUTPUT\X64\SetJump.iii c:\buildbs\201903\Build\AppPkg\DEBUG_VS2015x86\X64\MdePkg\Library\BaseLib\BaseLib\OUTPUT\X64\SetJump.i
	"$(NASM)" -Ic:\buildbs\201903\MdePkg\Library\BaseLib\X64\ $(NASM_INC) $(NASM_FLAGS) -o c:\buildbs\201903\Build\AppPkg\DEBUG_VS2015x86\X64\MdePkg\Library\BaseLib\BaseLib\OUTPUT\X64\SetJump.obj c:\buildbs\201903\Build\AppPkg\DEBUG_VS2015x86\X64\MdePkg\Library\BaseLib\BaseLib\OUTPUT\X64\SetJump.iii
就是说使用了c:\buildbs\201903\MdePkg\Library\BaseLib\X64\SetJump.nasm 进行编译。

3.具体代码在C:\BuildBs\201903\MdePkg\Library\BaseLib\X64\SetJump.nasm

;------------------------------------------------------------------------------
; UINTN
; EFIAPI
; SetJump (
;   OUT     BASE_LIBRARY_JUMP_BUFFER  *JumpBuffer
;   );
;------------------------------------------------------------------------------
global ASM_PFX(SetJump)
ASM_PFX(SetJump):
    push    rcx
    add     rsp, -0x20
    call    ASM_PFX(InternalAssertJumpBuffer)

完整的例子代码在这里下载

批处理请求管理员权限

当我们使用 RW Everything 这样的软件时,会自动请求管理员权限。

相比之下,我们使用批处理文件调用FITW 刷写 BIOS工具的时候不会出现这样的提示,又忘记使用管理员权限打开 CMD 窗口,这样会导致执行失败。最近看到了一个好用的批处理,可以在批处理文件中直接像 RW 这样来请求管理员权限,这样能够避免忘记使用管理员权限打开的问题。

批处理来自 “在批处理中提升权限 (UAC开启状态下)”【参考1】,根据作者的原文应该是翻译自https://sites.google.com/site/eneerge/home/BatchGotAdmin

@echo off

:: BatchGotAdmin
:-------------------------------------
REM  --> Check for permissions
>nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32\config\system"

REM --> If error flag set, we do not have admin.
if '%errorlevel%' NEQ '0' (
    echo Requesting administrative privileges...
    goto UACPrompt
) else ( goto gotAdmin )

:UACPrompt
    echo Set UAC = CreateObject^("Shell.Application"^) > "%temp%\getadmin.vbs"
    echo UAC.ShellExecute "%~s0", "", "", "runas", 1 >> "%temp%\getadmin.vbs"

    "%temp%\getadmin.vbs"
    exit /B

:gotAdmin
    if exist "%temp%\getadmin.vbs" ( del "%temp%\getadmin.vbs" )
    pushd "%CD%"
    CD /D "%~dp0"
:--------------------------------------

测试结果:

参考:

1. https://blog.csdn.net/wangjia184/article/details/7488341

Step to UEFI (180)GetVbtData 取得 Vbt

之前的《Shell 下读取 VBT》【参考1】给出了一个读取VBT 的方法。Simon 留言表示可以使用 PLATFORM_GOP_POLICY_PROTOCOL提供的 GetVbtData 。可以在\Vlv2DeviceRefCodePkg\ValleyView2Soc\NorthCluster\Include\Protocol\PlatformGopPolicy.h 中看到如下定义:

#define EFI_PLATFORM_GOP_POLICY_PROTOCOL_GUID \
  { 0xec2e931b, 0x3281, 0x48a5, 0x81, 0x7, 0xdf, 0x8a, 0x8b, 0xed, 0x3c, 0x5d }
#define PLATFORM_GOP_POLICY_PROTOCOL_REVISION_01 0x01
#define PLATFORM_GOP_POLICY_PROTOCOL_REVISION_02 x0222
#pragma pack(1)

typedef enum {
  LidClosed,
  LidOpen,
  LidStatusMax
} LID_STATUS;

typedef enum {
  Docked,
  UnDocked,
  DockStatusMax
} DOCK_STATUS;

typedef
EFI_STATUS
(EFIAPI *GET_PLATFORM_LID_STATUS) (
  OUT LID_STATUS *CurrentLidStatus
  );

typedef
EFI_STATUS
(EFIAPI *GET_VBT_DATA) (
  OUT EFI_PHYSICAL_ADDRESS *VbtAddress,
  OUT UINT32 *VbtSize
  );

#pragma pack()

typedef struct _PLATFORM_GOP_POLICY_PROTOCOL {
  UINT32                             Revision;
  GET_PLATFORM_LID_STATUS            GetPlatformLidStatus;
  GET_VBT_DATA                       GetVbtData;
} PLATFORM_GOP_POLICY_PROTOCOL;

根据上述定义编写代码,首先搜索 PLATFORM_GOP_POLICY_PROTOCOL ,然后调用它提供的GetVbtData 函数。

/** @file
    A simple, basic, EDK II native, "hello" application to verify that
    we can build applications without LibC.

    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  <Protocol/SimpleFileSystem.h>
#include  <Library/UefiBootServicesTableLib.h> //global gST gBS gImageHandle
#include  <Library/ShellLib.h>
#include  <Library/IoLib.h>

#include  "PlatformGopPolicy.h"

EFI_GUID gPlatformGOPPolicyGuid      = 
        { 0xec2e931b, 0x3281, 0x48a5, 
                { 0x81, 0x07, 0xdf, 0x8a, 0x8b, 0xed, 0x3c, 0x5d } };

//                        
//Save memory address to a file                        
//
EFI_STATUS 
SaveToFile(
        IN UINT8 *FileData, 
        IN UINTN FileDataLength)
{
    EFI_STATUS          Status;
    EFI_FILE_PROTOCOL   *FileHandle;
    UINTN               BufferSize;
    EFI_FILE_PROTOCOL   *Root;
    EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *SimpleFileSystem;

    Status = gBS->LocateProtocol(
                &gEfiSimpleFileSystemProtocolGuid, 
                NULL,
                (VOID **)&SimpleFileSystem);
                
    if (EFI_ERROR(Status)) {
            Print(L"Cannot find EFI_SIMPLE_FILE_SYSTEM_PROTOCOL \r\n");
            return Status;
    }

    Status = SimpleFileSystem->OpenVolume(SimpleFileSystem, &Root);
    if (EFI_ERROR(Status)) {
        Print(L"OpenVolume error \r\n");
        return Status;
    }
    Status = Root->Open(
                Root, 
                &FileHandle, 
                L"dumpvbt.bin",
                EFI_FILE_MODE_READ |
                EFI_FILE_MODE_WRITE | 
                EFI_FILE_MODE_CREATE, 
                0);
    if (EFI_ERROR(Status)){
        Print(L"Error Open NULL  [%r]\n",Status);
        return Status;
    }
    
    BufferSize = FileDataLength;
    Status = FileHandle->Write(FileHandle, &BufferSize, FileData);
    if (EFI_ERROR(Status)){
        Print(L"Error write [%r]\n",Status);
        return Status;
    }
    else Print(L"VBT has been saved to 'dumpvbt.bin' \n");
    
    FileHandle->Close(FileHandle);
    
    return Status;
}


/***
  Print a welcoming message.

  Establishes the main structure of the application.

  @retval  0         The application exited normally.
  @retval  Other     An error occurred.
***/
INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  )
{
        PLATFORM_GOP_POLICY_PROTOCOL    *mPlatformGopPolicyProtocl;
        EFI_STATUS              Status;
        EFI_PHYSICAL_ADDRESS    VbtAddress;
        UINT32                  VbtSize;
        
        Print(L"*************************************************\n");
        Print(L"* VBT dump tool By Platform GOP Policy Protocol *\n");
        Print(L"*                                 May 4th 2019  *\n");
        Print(L"*                      Powered by www.lab-z.com *\n");
        Print(L"*************************************************\n");
        
        //
        // Locate the Cpu Arch Protocol.
        //
        Status = gBS->LocateProtocol(
                        &gPlatformGOPPolicyGuid, 
                        NULL, 
                        &mPlatformGopPolicyProtocl);
        if (EFI_ERROR (Status)) {
                Print(L"Can't find EFI_PLATFORM_GOP_POLICY_PROTOCOL_\n");
                return Status;
        }        
        
        mPlatformGopPolicyProtocl->GetVbtData(&VbtAddress,&VbtSize);
        SaveToFile((UINT8 *)VbtAddress,VbtSize);
        
        return(0);
}

在 Shell 下运行之后会生成dumpvbt.bin ,内容就是当前使用的 VBT。

完整的代码和 EFI 文件下载:

这个方法和之前相比优点是避免了计算便宜之类的问题更加简单明确,可以在一定程度上克服不同Chipset之间的差别;缺点是:并非公开的 Protcol 在使用的时候可能存在一定风险。

最后,再次感谢 Simon 先生的建议。

参考:

1. http://www.lab-z.com/stu175vbt/  Step to UEFI (175)Shell 下读取 VBT

UEFI Tips: 嵌入 Asm 汇编需要特别注意的地方

之前介绍过,现在的 VS 不支持直接在代码中嵌入汇编,因此需要单独将汇编语句写在独立的 Asm 中然后在编译过程中会 Link 到生成的 EFI 文件中。最近碰到了一个奇怪的问题:明明写好的 Asm 在编译的 Link 过程中会提示无法找到对应的函数。经过研究最终确定是因为Asm 文件的名称导致的。

例如,之前的 “新指令 RDRAND”【参考1】文章中 INF 定义如下

[Sources.IA32]
  IA32/RdRandWord.c
  IA32/AsmRdRand.asm

[Sources.X64]
  X64/RdRandWord.c
  X64/AsmRdRand.asm

如果你手抖,命名成 X64/RdRandWord.asm 就会出现前面提到的问题。并且根据错误提示一直无法确定原因。

Intel CCA/DBC简介

本文根据《软件调试(第2版)》卷1:硬件基础“第七章 JTAG 调试”编写。建议有兴趣的朋友购买一本来学习。对于 Firmware 工程师来说,从底向上学习是一个很好的方向。

现在的X86变得日益复杂,如何进行有效的Debug 也日渐成为一个难题。为此 Intel 在芯片组或者 CPU 上预留了一个称作DCI (Intel® Direct Connect Interface)的Debug 接口。这个接口使用 USB 3.0一模一样的外部连接。CPU 内部有切换器,当Chipset 发现外部有设备和他握手,就将原本的USB信号切换为DCI 的信号。这样无需额外的预留就能实现Debug。

在DCI 出现之前,Intel 使用J-Tag 接口。当时的 Debug 盒子是下面这样,叫做 In-Target Proble,简称 ITP。  当年价格在 3000刀,现在好像没有这么贵了。之前我在的公司买了一个,老板恨不得把它供起来,一年也用不到几次。最后不知道什么原因它的适配器坏掉了。我去询问价格,得到的答案是适配器90刀,然后90天发货……..当然对方也很nice 的告诉我可以去中关村配一个电压相同功率差不多的也能用。后来回报上去之后老板左右不定,不想花钱和时间又怕损坏………最终这个设备束之高阁了。

图片来自【参考1】

为了Debug,主板上同时必须预留下面这样的 J Tag 接口。显而易见,说服HW工程师在主板上预留这样的接口需要花费极大的口舌,至于说服老板在量产板子上焊接这样的接口几乎是不可能的事情。因此,串口一直是BIOS工程师的最爱。

图片来自【参考2】

现在的Debug 盒子长得是下面这样,全称是“Intel SVT Closed Chassis Adapter”,缩写 “CCA”(Intel Silicon View Technology Closed Chassis Adapter),价格390刀。更通俗称作“DCI 蓝盒子”。通过这个设备可以绕开 CPU 直接和硬件打交道。这样,当 CPU 出现问题的时候,比如:CPU Hang了,工程师有机会来读取一些寄存器值…….

来自【参考3】

除此之外还有长得类似 WinDBG 线的 DbC( debug class ) 线,同样是插在 USB 3.0 口上就能进行Debug。

图片来自参考4

相比前面的 CCA ,可以看出 DBC 更见简单便宜,但是在Debug 的时候可能会出现无法连接的情况,比如停得太早或者 XHCI 出现问题直接死掉了。因此,如果有可能尽量首选 CCA。

上述设备的典型应用如下(来自文章开始提到的《软件调试》 7.4.7)

正如本章开头所说的,硬件调试工具通常用来解决软件调试器难以解决的问题,以下是使用 JTAG 方式调试的一些典型场景。

(1)调试系统固件代码,包括BIOS代码,EFI代码以及支持AMT技术的ME(Management Engine)代码。

(2)调试操作系统的启动加载程序(Boot Loader),以及系统临界状态的问题,比如进入睡眠和从睡眠状态恢复过程中发生的问题。

(3)软件调试器无法调试的其他情况,比如开发软件调试器时调试实现调试功能的代码(例如Windows的内核调试引擎),以及调试操作系统内核的中断处理函数,任务调度函数等。

(4)观察CPU的微状态,比如CPU的ACPI 状态(C State)。

作为BIOS工程师难得申请一些设备,所以如果有可能尽量申请一个。毕竟“工具善其事,必先利其器”,工具简单直观能省去很多麻烦。

参考:

  1. https://habrahabr.ru/company/pt/blog/341946/
  2. https://minnowboard.org/add-ons/debugger-lure
  3. https://designintools.intel.com/Silicon_View_Technology_Closed_Chassis_Adapter_p/itpxdpsvt.htm
  4. https://designintools.intel.com/product_p/itpdciamcm1mu.htm

C# 取得本机 IP

通常情况下,一台电脑不止一个 IP,因此需要考虑枚举出所有的 IP.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;

namespace tmpshowip
{
    class Program
    {
        static void Main(string[] args)
        {
            string hostName = Dns.GetHostName(); // Retrive the Name of HOST  
            for (int i = 0; i < Dns.GetHostByName(hostName).AddressList.Length; i++)
            {
                // Get the IP  
                string myIP = Dns.GetHostByName(hostName).AddressList[i].ToString();
                Console.WriteLine("My IP Address is :" + myIP);
            }
            Console.ReadKey();
        }
    }
}

参考:

1.https://www.codeproject.com/questions/399318/how-to-get-user-ip-address-using-csharp

Step to UEFI (179)Shell下 EC Ram 读取工具

最近工作需要在 UEFI  Shell 下Check EC Ram 的设定,发现手上只有 Windows 下的读取工具(RW Everything)。于是研究了一下如何在Shell 读取 EC Ram。

 根据【参考1】读取的流程如下:

Port 66 Commands

There are also some EC commands that use ports 0x66 and 0x62. Some of these are standard ACPI commands as defined in the external ACPI spec, others are custom.

The port 66 protocol is essentially the standard ACPI EC interface protocol.

1. Wait for port66.IBF = 0

2. Write command byte to port 66.

3. For each outgoing data or address byte:

3a. Wait for port66.IBF = 0

3b. Write data or address byte to port 62.

4. For each incoming data byte:

4a. Wait for port66.OBF = 1

4b. Read data byte from port 62.

5. If the command requires no data or address bytes, you can determine when the command was accepted/executed by waiting for port66.IBF=0.

同时 ACPI 定义的通用 Command如下:

ACPI-defined port 66 commands

0x80 Read EC (write 0x80 to port 66, write address byte to port 62, read data byte from port 62)

0x81 Write EC (write 0x81 to port 66, write address byte to port 62, write data byte to port 62)

0x82 Burst Enable (write 0x82 to port 66, read data byte from port 62 – the data byte is “burst ACK”, value 0x90)

0x83 Burst Disable (write 0x83 to port 66, wait for port66.IBF=0)

0x84 Query EC (i.e. read SCI event queue) (write 0x84 to port 66, read data byte from port 62). When the data byte is 0, it means that the SCI event queue is empty.

最终根据上述资料,编写一个 Application 如下:

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

extern  EFI_SYSTEM_TABLE    *gST;
extern  EFI_BOOT_SERVICES   *gBS;

#define TIMEOUT         0xFFFF
#define ECCOMMAND       0x66
#define ECSTATUS        0x66
#define ECDATA          0x62
#define EC_S_OBF           BIT0
#define EC_S_IBF           BIT1
#define ECREADCMD          0x80

UINT8   MemBuffer[16][16];

void   WaitIBF() {
  UINT32 Status;
  UINTN Count;

  Count = 0;
  Status = 0;

  Status = IoRead8 (ECSTATUS);
  while (((Status & EC_S_IBF) != 0)||(Count>TIMEOUT)) {
    Status = IoRead8 (ECSTATUS);
    Count++;
  }
}

void    WaitOBF() {
  UINT32 Status;
  UINTN Count;

  Count = 0;
  Status = 0;

  Status = IoRead8 (ECSTATUS);
  while (((Status & EC_S_OBF) == 0)||(Count>TIMEOUT)) {
    Status = IoRead8 (ECSTATUS);
    Count++;
  }
}

UINT8 ReadECRam(UINT8 Index) {
  WaitIBF();               //1
  IoWrite8(ECCOMMAND,0x80);//2
  WaitIBF();               //3a
  IoWrite8(ECDATA, Index); //3b
  WaitOBF();               //4a
  return IoRead8(ECDATA); //4b
}

void GetData()
{
        UINT8   i,j;
        for (i=0;i<16;i++)
          for (j=0;j<16;j++) {
             MemBuffer[i][j]=ReadECRam(i*16+j);
          }
}
void ShowData()
{
        UINT8   i,j;
        Print(L"    ");
        for (i=0;i<16;i++) Print(L"%02X ",i);
        Print(L"\n");
        for (i=0;i<16;i++) {
                Print(L"%02X: ",i);
           for (j=0;j<16;j++) {
                Print(L"%02X ",MemBuffer[i][j]);
           }
           Print(L"\n");
        }
        Print(L"\n");
}
/***
  Print a welcoming message.

  Establishes the main structure of the application.

  @retval  0         The application exited normally.
  @retval  Other     An error occurred.
***/
INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  )
{
  EFI_INPUT_KEY         Key;

  Key.ScanCode=SCAN_NULL;
  while (SCAN_UP!=Key.ScanCode)
    {
        gST->ConOut->ClearScreen(gST->ConOut);
        GetData();
        ShowData();
        gST -> ConIn -> ReadKeyStroke(gST->ConIn,&Key);   
        Print(L"Press Arrow-Up to exit\n");
        gBS->Stall(1000000UL);
    }

    return(0);
}

在实体机上运行结果如下(按向上键退出):

源代码和Application(X64)下载:

参考:

1. http://wiki.laptop.org/go/Ec_specification