FireBeetle 直接放声(DAC篇)

声音,是物体振动产生的,通常情况下通过空气将震动传递到耳朵我们就能听到了声音。常见的喇叭就是一种将电能转换为机械能的器件。

FireBeetle 核心是 ESP32-WROOM-32D, 主控频率高达240Mhz, Flash Rom 有 16MB。这里介绍一种直接通过它来播放声音的方法。用这种方法可以直接通过喇叭来播放音乐,但是因为 ESP32 输出功率有限,直接推动喇叭产生的声音很小。为此,这里使用“Gravity: 带功放喇叭模块”来实现更大的声音输出。

Gravity: 带功放喇叭模块

基本原理是将数据存储在 FireBeetle 的Flash 上,然后通过 ESP32 的DAC 直接输出之。

具体步骤如下:

第一步,将音乐转化为8位单声道。我是用 Golden Wave ,将罗大佑的 “恋曲1990.mp3“转为 8000Hz 8 Bits的Wav 格式。

Golden Wave 打开音频文件

选择 Resample
使用 8000Hz 重新采样

注意,这里必须重新采样为 8000Hz。直接另存为的话实际上并没有设置为 8000Hz。

保存为8Bits 单声道

GoldenWave 自动重新打开文件

重新加载处理后的 8000Hzm 8Bits Wave 音频数据

第二步,使用Bin2C 将这个 Wav 生成C 语言的头文件。命令如下:

bin2c.exe -o audio.h 19908bits.wav

实际上数据中包含了 wav的文件头,但是因为数据量不大,对播放几乎没有影响,所以这里也没有额外处理。

这样,我们就有了这个歌曲的数据。特别需要注意的有下面两点:

1.需要设置编译模式为  3MB APP/9MB FATFS 模式

2.原始的 Wav 最好不要超过 2.7MB(2,831,155.2Bytes, 0x2B3333Bytes),否则会超过最大程序的限制。这次我使用的 WAV 是 2,527,766 字节的,编译后结果如下:

Sketch uses 2746845 bytes (87%) of program storage space. Maximum is 3145728 bytes.

Global variables use 15372 bytes (4%) of dynamic memory, leaving 312308 bytes for local variables. Maximum is 327680 bytes.

第三步,我们需要一些基本的测试。首先,编写一个循环,使用 dacwrite() 输出数值,输出2000000次花费了11102ms ,也就是说一次 dac 输出需要花费 0.005551ms。前面提到使用的 Wav采样率是 8000Hz。这样一个周期是 1/8000=0.125ms。 因此,为了重建一个声音,我们需要 0.125/0.005551=22.5184次 dacwrite。

第四步,编写代码:

#include "audio\SoundData.h"
void setup() {
}
void loop() {
  for (unsigned int i=0;i<2527766;i++) {
    for (int j=0;j<21;j++) dacWrite(25,WarOfWorldsWav[i]);
  }
}

我们将音频数据放在audio目录下的SoundData.h文件中,这样每次Arduino 打开ino 文件的时候并不会一起打开音频数据,否则因为音频数据很大,会耽误很多时间,有时候甚至会导致Arduino 编译器崩溃。另外,和前面提到的22次 dacwrite 不同,代码使用的是21次,因为for循环有一些开销,所以实际测量下来22次有一点点慢。

第五步,Gravity: 带功放喇叭模块,上面有3个线,VCC和GND 连接到 FireBeetle上,信号输入Pin 连接到 FireBeetle D2 (IO25)。

下载代码后即可播放出音乐了。

很明显,上述方法足够简单,能够存储 2.7*1024*1024/8000=354秒的音频。在要求不高的场合下完全能够满足要求。

可以直接连接喇叭,只是声音小一些

8Bits Wave 格式的歌曲:

完整的代码和数据:

工作的视频:

https://www.bilibili.com/video/BV1Df4y1x7WF?share_source=copy_web

FireBeetle 直接放音(PWM篇)

前面介绍了FireBeetle 通过 DAC 来播放音频,除此之外,还可以使用 PWM 方式来播放音频。

关于 PWM动力老男孩在“Arduino系列教程之 – PWM的秘密(上)”【参考1】有介绍,对于我们来说,能用到的就是下面这一段:

PWM是用占空比不同的方波,来模拟“模拟输出”的一种方式。靠,这个太拗口了,简而言之就是电脑只会输出0和1,那么想输出0.5怎么办呢?于是输出01010101….,平均之后的效果就是0.5了。早这么说就了然了嘛。

比如,当前最高电压是5V,如果输出50%的PWM信号,可以当作 2.5V 的信号输出。对于 ESP32来说,有对 PWM 的直接支持【参考2】。

Arduino core for the ESP32 并没有一般 Arduino 中用来输出 PWM analogWrite(pin, value) 方法,取而代之的 ESP32 有一个 LEDC ,设计是用来控制 LED

ESP32 的 LEDC 总共有16个路通道(0 ~ 15),分为高低速两组,高速通道(0 ~ 7)由80MHz时钟驱动,低速通道(8 ~ 15)由 1MHz 时钟驱动。

对于我们来说,用到的函数有下面3个:

ledcSetup(uint8_t channel, double freq, uint8_t resolution_bits)

分别设定使用的通道(Channel),PWM 的频率, PWM 的分辨率。比如,我们设定 1Hz 的频率,然后分辨率为4Bit,那么就可以设置从 0 到15,一共16个PWM值。可以看出,分辨率越高,可以细分出更多的 PWM值。

ledcWrite(uint8_t channel, uint32_t duty)

对通道设定当前的占空比(duty)

ledcAttachPin(uint8_t pin, uint8_t channel)

将 LEDC 通道绑定到指定 IO 口上

这样,我们就得到了一个和之前 DAC 很像的代码:

#include "audio\SoundData.h"

int freq = 8000*256;    // 频率
int channel = 0;    // 通道
int resolution = 8;   // 分辨率

const int led = 25;
void setup() {
  ledcSetup(channel, freq, resolution); // 设置通道
  ledcAttachPin(led, channel);  // 将通道与对应的引脚连接‘
  Serial.begin(115200);
}

void loop() {
  for (unsigned int i=0;i<2527766;i++) {
     ledcWrite(channel, WarOfWorldsWav[i]);
     delayMicroseconds(120);
  }
}

完整的代码和数据下载:

前面提到了,PWM 支持更高的分辨率,因此,我们可以尝试播放16Bits 的音频。最简单的想法,直接将频率设定为 8000Hz,然后分辨率为16位。但是实际测试下来这样无法工作,经过研究,频率和分辨率之间有一定的限制关系【参考3】。在 8000Hz 下能够达到最高分辨率是 13bits。最终实验表明使用 12Bits 分辨率 8000Hz 可以接收,再高噪音会较大。此外,16bits 的 WAV 和 8Bits 的还有一个很大的区别在于:前者是有符号数值,后者是无符号数值。比如:0x8001 实际上表示的是 -1。因此代码中取出数值后需要加上 0x8000 再做处理。因此代码中取出数值后需要加上 0x8000 再做处理。另外,因为 16Bits 相对于 8Bits 数据量是直接翻倍了,导致无法在 Flash 中放下全部文件,为此,这次实验用到的16Bits音频数据只是截取了部分歌曲。

参考:

The maximum PWM frequency with the currently used ledc duty resolution of 10 bits in PWM module is 78.125KHz.
The duty resolution can be lowered down to 1 bit in which case the maximum frequency is 40 MHz, but only the duty of 50% is available.
For duty resolution of 8 buts, the maximal frequency is 312.5 kHz.
The available duty levels are (2^bit_num)-1, where bit_num can be 1-15.
The maximal frequency is 80000000 / 2^bit_num
In my MicroPython implementation, I'm currently working on enabling user selectable and/or automatic duty resolution and higher maxumum frequencies.

4.手册上有描述

Magnetic Core Memory

在上古的电脑时代,计算机使用 Magnetic Core Memory 来作为 RAM 存储设备。

中文名称是“磁芯存储器”。简单的说,这种磁芯有着不同的磁化方向。用这种方式可以记录0 和 1 两种状态。当下方的导线通过电流时,不同磁化方向会对电流有着不同的影响,这样就能通过经过的电流大小得出当前存储的装态。

MindShare

这种装置可以看作时现代 DRAM 的雏形。有兴趣的朋友还可以阅读下面的文章:

1.https://baike.baidu.com/item/%E7%A3%81%E8%8A%AF%E5%AD%98%E5%82%A8%E5%99%A8/10189808?fr=aladdin 磁芯存储器

2. https://zhuanlan.zhihu.com/p/144628785 磁芯存储:统治存储领域20年

3.http://www.elecfans.com/d/1277911.html 带你了解磁芯存储器

ESP32 作为蓝牙音源

前面介绍过 ESP32 作为蓝牙音频接收端(蓝牙音箱),这里介绍它作为蓝牙音频的播放端。

首先需要确定蓝牙接收器的名称,用笔记本电脑连接后,可以再设备管理器中看到,这里我使用的是一款蓝牙耳机,名称是“JABRA TALK”:

Jabra Talk 蓝牙耳机

接下来需要安装 ESP32-A2DP-master 这个库。下面的代码是从这个库的Example 中修改而来,代码如下:

/*
  Streaming of sound data with Bluetooth to an other Bluetooth device.
  We provide the complete sound data as a simple c array which 
  can be prepared e.g. in the following way

  - Open any sound file in Audacity. Make sure that it contains 2 channels
    - Select Tracks -> Resample and select 44100
    - Export -> Export Audio -> Header Raw ; Signed 16 bit PCM
  - Convert to c file e.g. with "xxd -i file_example_WAV_1MG.raw file_example_WAV_1MG.c"
    - add the const qualifier to the array definition. E.g const unsigned char file_example_WAV_1MG_raw[] = {
  
  Copyright (C) 2020 Phil Schatzmann
  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.
  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "BluetoothA2DPSource.h"
#include "StarWars30.h"
BluetoothA2DPSource a2dp_source;
//SoundData *data = new TwoChannelSoundData((Channels*)StarWars10_raw,StarWars10_raw_len/4);
SoundData *data = new OneChannelSoundData((int16_t*)StarWars30_raw, StarWars30_raw_len/2);

void setup() {
  Serial.begin(115200);
  Serial.println("Start");
  a2dp_source.start("JABRA TALK");    
}

void loop() {
  if (a2dp_source.isConnected()==true) {
      Serial.println("Connected!");
      if (a2dp_source.hasSoundData()==true) {
          Serial.println("Has sound!");
        }
      else {
          Serial.println("No sound!");
          a2dp_source.writeData(data);   
      }  
    }
    else {
      Serial.println("Not connected!");
    }
  delay(2000);  
}

特别注意,因为代码有音频数据需要特别选择 Huge APP 模式:

选择 Huge APP 模式

测试使用的板子是 ESP-WROOM-32,特点是价格偏移兼容性还不错(同样的我试验了 TinkerNode,播放时有卡顿,似乎后台有人一直占用 Soc):

ESP-WROOM-32 很便宜
蓝牙耳机和 ESP32

测试的视频:

https://www.bilibili.com/video/BV1wa411A7et/

特别注意:连接时需要比较有耐心多次尝试,先将耳机设置为配对模式,然后ESP32上电。从资料来看,这样的搭配似乎有兼容性问题,淘宝上的卖家都不承诺蓝牙音频端能够兼容客户的蓝牙接收端。

========================

2023年5月10日

Step to UEFI (230)OVMF 一个FV 的打包过程分析

前面介绍了如何EDK2在编译的最后过程中使用了 GenFds 进行打包。分析的目标是 QEMU 的 BIOS 文件 OVMF.FD,使用 UEFITool NE 打开之后,可以看到有三个 FV ,我们以中间的为例,分析它的生成方法。整体分析过程比较枯燥,对于大多数人来说了解大致的步骤就可以了。

首先,在 FDF 文件中,给出了这个 FV 的GUID 可以看到:

[FV.FVMAIN_COMPACT]
FvNameGuid         = 48DB5E17-707C-472D-91CD-1613E7EF51B0
FvAlignment        = 16
ERASE_POLARITY     = 1
MEMORY_MAPPED      = TRUE
STICKY_WRITE       = TRUE
LOCK_CAP           = TRUE
LOCK_STATUS        = TRUE
WRITE_DISABLED_CAP = TRUE
WRITE_ENABLED_CAP  = TRUE
WRITE_STATUS       = TRUE
WRITE_LOCK_CAP     = TRUE
WRITE_LOCK_STATUS  = TRUE
READ_DISABLED_CAP  = TRUE
READ_ENABLED_CAP   = TRUE
READ_STATUS        = TRUE
READ_LOCK_CAP      = TRUE
READ_LOCK_STATUS   = TRUE

上面的FV 是由两个 Section 构成的,一个是 PEIFV,另一个是 DXEFV:

1.下面是对于 PEIFV 的定义:

PEIVFV 定义
[FV.PEIFV]
FvNameGuid         = 6938079B-B503-4E3D-9D24-B28337A25806
BlockSize          = 0x10000
FvAlignment        = 16
ERASE_POLARITY     = 1
MEMORY_MAPPED      = TRUE
STICKY_WRITE       = TRUE
LOCK_CAP           = TRUE
LOCK_STATUS        = TRUE
WRITE_DISABLED_CAP = TRUE
WRITE_ENABLED_CAP  = TRUE
WRITE_STATUS       = TRUE
WRITE_LOCK_CAP     = TRUE
WRITE_LOCK_STATUS  = TRUE
READ_DISABLED_CAP  = TRUE
READ_ENABLED_CAP   = TRUE
READ_STATUS        = TRUE
READ_LOCK_CAP      = TRUE
READ_LOCK_STATUS   = TRUE

APRIORI PEI {
  INF  MdeModulePkg/Universal/PCD/Pei/Pcd.inf
}

2.下面是 DXEFV 的定义

DXEFV的定义

[FV.DXEFV]

FvForceRebase      = FALSE

FvNameGuid         = 7CB8BDC9-F8EB-4F34-AAEA-3EE4AF6516A1

BlockSize          = 0x10000

FvAlignment        = 16

ERASE_POLARITY     = 1

MEMORY_MAPPED      = TRUE

STICKY_WRITE       = TRUE

LOCK_CAP           = TRUE

LOCK_STATUS        = TRUE

WRITE_DISABLED_CAP = TRUE

WRITE_ENABLED_CAP  = TRUE

WRITE_STATUS       = TRUE

WRITE_LOCK_CAP     = TRUE

WRITE_LOCK_STATUS  = TRUE

READ_DISABLED_CAP  = TRUE

READ_ENABLED_CAP   = TRUE

READ_STATUS        = TRUE

READ_LOCK_CAP      = TRUE

READ_LOCK_STATUS   = TRUE

APRIORI DXE {

  INF  MdeModulePkg/Universal/DevicePathDxe/DevicePathDxe.inf

  INF  MdeModulePkg/Universal/PCD/Dxe/Pcd.inf

  INF  OvmfPkg/AmdSevDxe/AmdSevDxe.inf

!if $(SMM_REQUIRE) == FALSE

  INF  OvmfPkg/QemuFlashFvbServicesRuntimeDxe/FvbServicesRuntimeDxe.inf

!endif

}

这里可以使用 UEFITool 直接将内容解压出来,例如:

必须选择 Extract Body

可以用 Z7 查看解压出来的文件,但是7Z 只能看到 PEIFV 看不到DXEFV, 我猜测是因为格式原因,只能看到前面一半:

7Z 打开查看的结果

具体的 DXEFV 和 PEIFV 可以在 \Build\OvmfX64\DEBUG_VS2015x86\FV 下面看到:

使用这两个文件生成的

有了这两个文件,就可以生成出现在 OVMF.FD  中的 FV 了。为了便于描述,使用下面的流程图:

生成的流程和关系

CMD1:   生成 9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.1fv.sec

GenSec -s EFI_SECTION_FIRMWARE_VOLUME_IMAGE -o d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792FVMAIN_COMPACT\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.1fv.sec d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\PEIFV.Fv

CMD2:   生成9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.2fv.sec

GenSec -s EFI_SECTION_FIRMWARE_VOLUME_IMAGE -o d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792FVMAIN_COMPACT\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.2fv.sec d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\DXEFV.Fv

CMD3:  将上面两个文件合成为一个文件 9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.guided.dummy

GenSec –sectionalign 128 –sectionalign 16 -o d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792FVMAIN_COMPACT\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.guided.dummy d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792FVMAIN_COMPACT\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.1fv.sec d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792FVMAIN_COMPACT\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.2fv.sec

CMD4: 对上面的文件进行压缩,生成 9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.tmp 文件,从 11MB 压缩到了1MB左右

LzmaCompress -e -o d:\\i2c\\Build\\OvmfX64\\DEBUG_VS2015x86\\FV\\Ffs\\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792FVMAIN_COMPACT\\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.tmp d:\\i2c\\Build\\OvmfX64\\DEBUG_VS2015x86\\FV\\Ffs\\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792FVMAIN_COMPACT\\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.guided.dummy

CMD5: 继续打包生成 9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.tmp

GenSec -s EFI_SECTION_GUID_DEFINED -g EE4E5898-3914-4259-9D6E-DC7BD79403CF -r PROCESSING_REQUIRED -o d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792FVMAIN_COMPACT\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.guided d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792FVMAIN_COMPACT\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.tmp

CMD6:生成 FFS文件,9E21FD93-9C72-4c15-8C4B-E77F1DB2D792.ffs

GenFfs -t EFI_FV_FILETYPE_FIRMWARE_VOLUME_IMAGE -g 9E21FD93-9C72-4c15-8C4B-E77F1DB2D792 -o d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792FVMAIN_COMPACT\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792.ffs -i d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792FVMAIN_COMPACT\9E21FD93-9C72-4c15-8C4B-E77F1DB2D792SEC1.guided

CMD7:最终生成  FVMAIN_COMPACT.Fv,用 Beyond Compare 可以看到这个是 OVMF.FD 的一部分。

GenFv -a d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\FVMAIN_COMPACT.inf -o d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\FVMAIN_COMPACT.Fv -i d:\i2c\Build\OvmfX64\DEBUG_VS2015x86\FV\FVMAIN_COMPACT.inf

生成的FVMAIN_COMPACT.Fv 就是 OVMF 中间的一部分。

ESP32 搭配语音合成模块

之前介绍过SYN6288 模块【参考1】,这次配合 ESP32 实现随机生成一个时间,然后通过语音播放出来。

硬件使用的是ESP-WROOM-32 ESP-32S模块,语音模块的 RX 连接到 ESP32 的GPIO17:

String DataBuffer[11]={{"零"},{"一"},{"二"},{"三"},{"四"},{"五"},{"六"},{"七"},{"八"},{"九"},{"十"}};
                      
//存放转化后的汉字 Unicode值
char character[40];

#include <HardwareSerial.h>

//根据字符串计算计算出来的送到串口的值
char output[50];

void setup() {
  Serial.begin(115200);
  Serial2.begin(9600);
  delay(1000);
}

void loop() {
  Serial.println("generate");

  int hh=random(13),mm=random(60);
  String timeStr="";

  Serial.print("Generate time: ");
  Serial.print(hh);Serial.print(":");Serial.println(mm);

  if (hh>9) {timeStr+="十";
    if (hh>10) {timeStr+=DataBuffer[hh-10];}
  }
  else {timeStr+=DataBuffer[hh];}
  timeStr+="点";

  if (mm==0) {timeStr+="整";}
  else
    if (mm<10) {timeStr+="零";timeStr+=DataBuffer[mm];}
    else if (mm%10==0) {timeStr+=DataBuffer[mm/10]; timeStr+="十";}
         else {timeStr+=DataBuffer[mm/10];timeStr+="十";timeStr+=DataBuffer[mm%10];}
  if(mm!=0) {timeStr+="分";}
 
    //timeStr="十二点五十八分";
  //首先输出一次Arduino 原始字符串 UTF8 的值
  for (int i =0;i<timeStr.length()*3;i++) {
     Serial.print(timeStr[i]&0xFF,HEX);
      Serial.print(' ');
  }
  Serial.println(' ');

  //将 UTF8 转化为 Unicode
    for (int i =0;i<timeStr.length()/3;i=i+1) {
      character[i*2]=((timeStr[i*3]&0xF)<<4)+((timeStr[i*3+1]>>2)&0xF);
      character[i*2+1]=((timeStr[i*3+1]&0x3)<<6)+(timeStr[i*3+2]&0x3F);
      Serial.print(character[i*2]&0xFF,HEX);
      Serial.print(' ');
      Serial.print(character[i*2+1]&0xFF,HEX);
      Serial.print(' ');      
    } 
  Serial.println(""); 

  output[0]=0xFD;
  output[1]=(timeStr.length()/3*2+3)>>8;
  output[2]=((timeStr.length()/3*2+3)&0xFF);
  output[3]=0x01;
  output[4]=0x03;
  //把字符串定义搬过去
  for (int i=0;i<timeStr.length()/3*2;i++) {
      output[i+5]=character[i];
    } 
  //计算一个校验和  
  output[timeStr.length()/3*2+5]=output[0];
  for (int i=1;i<timeStr.length()/3*2+5;i++) {
      output[timeStr.length()/3*2+5]=output[timeStr.length()/3*2+5] ^ output[i];
    }   
  
  for (int i =0;i<timeStr.length()/3*2+6;i++) {
      Serial.print(output[i]&0xFF,HEX);
      Serial.print(' ');
      Serial2.write(output[i]);
    }     
  
Serial.println(' ');
    delay(3000);
}

参考:

1. http://www.lab-z.com/ttssyn/ TTS 真人发音 SYN6288 模块

Step to UEFI (229)继续研究修改QEMU 显示的版本号

前面提到过 OVMF 的 Setup 首页版本号显示的代码【参考1】,在 \MdeModulePkg\Application\UiApp\FrontPage.c

  SmbiosHandle = SMBIOS_HANDLE_PI_RESERVED;
  Status = Smbios->GetNext (Smbios, &SmbiosHandle, NULL, &Record, NULL);
  while (!EFI_ERROR(Status)) {
    if (Record->Type == SMBIOS_TYPE_BIOS_INFORMATION) {
      Type0Record = (SMBIOS_TABLE_TYPE0 *) Record;
      StrIndex = Type0Record->BiosVersion;
      GetOptionalStringByIndex ((CHAR8*)((UINT8*)Type0Record + Type0Record->Hdr.Length), StrIndex, &NewString);

      FirmwareVersionString = (CHAR16 *) PcdGetPtr (PcdFirmwareVersionString);
      if (*FirmwareVersionString != 0x0000 ) {
        FreePool (NewString);
        NewString = (CHAR16 *) PcdGetPtr (PcdFirmwareVersionString);
        UiCustomizeFrontPageBanner (3, TRUE, &NewString);
        HiiSetString (gFrontPagePrivate.HiiHandle, STRING_TOKEN (STR_FRONT_PAGE_BIOS_VERSION), NewString, NULL);
      } else {
        UiCustomizeFrontPageBanner (3, TRUE, &NewString);
        HiiSetString (gFrontPagePrivate.HiiHandle, STRING_TOKEN (STR_FRONT_PAGE_BIOS_VERSION), NewString, NULL);
        FreePool (NewString);
      }
    }

就是说出了从 SMBIOS 之外,还可以在 PCD 中直接给定。实验,在\OvmfPkg\OvmfPkgX64.dsc 文件中加入下面的代码

!if $(SMM_REQUIRE) == TRUE
  gUefiOvmfPkgTokenSpaceGuid.PcdSmmSmramRequire|TRUE
  gUefiCpuPkgTokenSpaceGuid.PcdCpuSmmEnableBspElection|FALSE
!endif

[PcdsFixedAtBuild]
#LABZ_Start
  gEfiMdeModulePkgTokenSpaceGuid.PcdFirmwareVersionString|L"Galileo 1.0.4"  
#LABZ_End

  gEfiMdeModulePkgTokenSpaceGuid.PcdStatusCodeMemorySize|1
  gEfiMdeModulePkgTokenSpaceGuid.PcdResetOnMemoryTypeInformationChange|FALSE
  gEfiMdePkgTokenSpaceGuid.PcdMaximumGuidedExtractHandler|0x10
  gEfiMdeModulePkgTokenSpaceGuid.PcdPeiCoreMaxFvSupported|6

编译之后,运行结果如下,可以看到新增加了  “Galileo 1.0.4”字样。

新加入了版本字符

但是,非常奇怪的是按照之前的方法,无法在 FFS 文件中找到对应的字符串。为了进一步研究,打开生成 COD 的功能。在 \MdeModulePkg\Application\UiApp\UiApp.inf 加入下面这样一行:

[BuildOptions]  
  MSFT:*_*_X64_CC_FLAGS = /FAsc /Od

再次编译,在

\Build\OvmfX64\DEBUG_VS2015x86\X64\MdeModulePkg\Application\UiApp\UiApp\DEBUG\AutoGen.c 有如下定义:

GLOBAL_REMOVE_IF_UNREFERENCED const UINT16 _gPcd_FixedAtBuild_PcdFirmwareVersionString[14] = {71, 97, 108, 105, 108, 101, 111, 32, 49, 46, 48, 46, 52, 0 };

进一步,在 \Build\OvmfX64\DEBUG_VS2015x86\X64\MdeModulePkg\Application\UiApp\UiApp\AutoGen.cod 可以看到下面的定义

;	COMDAT _gPcd_FixedAtBuild_PcdFirmwareVersionString
CONST	SEGMENT
_gPcd_FixedAtBuild_PcdFirmwareVersionString DW 047H
	DW	061H
	DW	06cH
	DW	069H
	DW	06cH
	DW	065H
	DW	06fH
	DW	020H
	DW	031H
	DW	02eH
	DW	030H
	DW	02eH
	DW	034H
	DW	00H
CONST	ENDS

特别注意,每一个字符是 2Bytes,因此,就是说和之前不同,这里字符串使用的是 Unicode 编码方式。因此,一个字符会使用2个Bytes来进行保存。再次到 FFS 中搜索,可以看到如下字样:

十六进制编辑器直接编辑 Unicode 字符

尝试修改之,然后用前面直接运行 GenFDS 的方法产生的 ROM 文件,运行结果如下:

修修改QEMU Setup 界面之后的结果

进一步研究,在【参考2】有介绍,我们定义的gEfiMdeModulePkgTokenSpaceGuid.PcdFirmwareVersionString 是 FixedAtBuild PCD的PCD 变量,编译时会像宏一样直接展开放在代码中。

参考:

1.http://www.lab-z.com/qemusetup/ QEMU Setup 首页研究

2. https://blog.csdn.net/jiangwei0512/article/details/80288001 BIOS/UEFI基础——PCD

学习 Verilog 的好去处:HDLBits

带着问题进行学习是掌握一门技术非常有效的方法。

https://hdlbits.01xz.net/ 网站提供了 Verilog 的一些题目,有兴趣的朋友可以尝试进行练习。同时这个网站在提供 Verilog 基础语法教程的同时,还能够在线仿真你的 Verilog 模块,将你的输出与正确的时序比较。

如果你在解题时遇到问题,还可以在知乎“HDLBits 中文导学”专栏中看到解析(比如我经常无法理解题目意思),还可以在评论中参加讨论。

https://zhuanlan.zhihu.com/c_1131528588117385216

Step to UEFI (228)QEMU Setup 首页研究

最近研究了一下 OVMF 项目的 Setup 首页界面,实现的方法挺有意思。

第一个问题,具体代码在什么地方,如果我想增加一行字应该如何实现。

OVMF 运行首页

经过研究,相关代码在  MdeModulePkg\Application\UiApp\FrontPage.c 中。首先,在 FrontPageVfr.Vfr 定义了8个字符串变量,例如:STR_CUSTOMIZE_BANNER_LINE4_LEFT。 在首页上面左右各有4个。在 UpdateFrontPageBannerStrings() 函数中动态填充这些字符串。例如:从 SMBIOS 中取得CPU 信息显示出来:

    if ((Record->Type == SMBIOS_TYPE_PROCESSOR_INFORMATION) && !FoundCpu) {
      Type4Record = (SMBIOS_TABLE_TYPE4 *) Record;
      //
      // The information in the record should be only valid when the CPU Socket is populated.
      //
      if ((Type4Record->Status & SMBIOS_TYPE4_CPU_SOCKET_POPULATED) == SMBIOS_TYPE4_CPU_SOCKET_POPULATED) {
        StrIndex = Type4Record->ProcessorVersion;
        GetOptionalStringByIndex ((CHAR8*)((UINT8*)Type4Record + Type4Record->Hdr.Length), StrIndex, &NewString);
        UiCustomizeFrontPageBanner (2, TRUE, &NewString);
        HiiSetString (gFrontPagePrivate.HiiHandle, STRING_TOKEN (STR_FRONT_PAGE_CPU_MODEL), NewString, NULL);
        FreePool (NewString);

        ConvertProcessorToString(Type4Record->CurrentSpeed, 6, &NewString);
        UiCustomizeFrontPageBanner (2, FALSE, &NewString);
        HiiSetString (gFrontPagePrivate.HiiHandle, STRING_TOKEN (STR_FRONT_PAGE_CPU_SPEED), NewString, NULL);
        FreePool (NewString);

        FoundCpu = TRUE;
      }
    }

知道了上面的实现,如果想实现增加一行字符串,实现非常简单:在  FrontPageStrings.uni 文件中修改字符串增加内容即可:

#string STR_CUSTOMIZE_BANNER_LINE4_LEFT  #language en-US  "www.lab-z.com"
                                         #language fr-FR  ""

运行结果:

可以看到上面增加了 www.lab-z.com 字样

第二个问题,研究一下BIOS 版本号的显示。取得的方法非常类似,同样的文件中,代码如下:

    if (Record->Type == SMBIOS_TYPE_BIOS_INFORMATION) {
      Type0Record = (SMBIOS_TABLE_TYPE0 *) Record;
      StrIndex = Type0Record->BiosVersion;
      GetOptionalStringByIndex ((CHAR8*)((UINT8*)Type0Record + Type0Record->Hdr.Length), StrIndex, &NewString);

      FirmwareVersionString = (CHAR16 *) PcdGetPtr (PcdFirmwareVersionString);
      if (*FirmwareVersionString != 0x0000 ) {
        FreePool (NewString);
        NewString = (CHAR16 *) PcdGetPtr (PcdFirmwareVersionString);
        UiCustomizeFrontPageBanner (3, TRUE, &NewString);
        HiiSetString (gFrontPagePrivate.HiiHandle, STRING_TOKEN (STR_FRONT_PAGE_BIOS_VERSION), NewString, NULL);
      } else {
        UiCustomizeFrontPageBanner (3, TRUE, &NewString);
        HiiSetString (gFrontPagePrivate.HiiHandle, STRING_TOKEN (STR_FRONT_PAGE_BIOS_VERSION), NewString, NULL);
        FreePool (NewString);
      }
    }

就是说,如果没有定义 PcdFirmwareVersionString 那么会从 SMBIOS 中抓取BIOS 版本显示出来。

在 \OvmfPkg\SmbiosPlatformDxe\SmbiosPlatformDxe.c 可以看到版本信息定义如下:

#define TYPE0_STRINGS \
  "EFI Development Kit II / OVMF\0"     /* Vendor */ \
  "0.0.0\0"                             /* BiosVersion */ \
  "02/06/2015\0"                        /* BiosReleaseDate */

可以通过直接修改上面代码的方式来实现修改版本号的目标。作为“寻常不走路”的有志青年,当然不会满足于使用这样的方式来修改版本号。下面介绍直接在二进制上修改这个版本号。

不走寻常路

在 Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\4110465d-5ff3-4f4b-b580-24ed0d06747aSmbiosPlatformDxe 目录下,可以找到 4110465d-5ff3-4f4b-b580-24ed0d06747a.ffs 这个文件。打开文件可以搜索到 “1.2.3”字样:

可以直接搜索到字符串

尝试修改为 “4.5.6” 之后,再次使用之前介绍过的 GenFds ,生成 FD 文件,运行后就可以发现字符发生了变化:

版本号发生了变化

这个实验也从侧面证明了 GenFds 是将需要的 FFS 文件集合在一起,然后压缩放入指定的文件中。