2026年2月更新,Step to UEFI 文章索引:
Step to UEFI (309)UEFI 下BMP转JPG 的程序
这次是一个比较完美的程序,可以在UEFI Shell 下将BMP图片转为 JPEG图片。项目来自 https://github.com/MikeWang000000/wsjpeg 。同样是一个单文件项目。
编译的时候加入了一些关闭 Warning 的动作,完整的 INF如下:
[Defines]
INF_VERSION = 0x00010006
BASE_NAME = BMP2JPG
FILE_GUID = 4ea97c46-2026-0429-b445-747010f3ce5f
MODULE_TYPE = UEFI_APPLICATION
VERSION_STRING = 0.1
ENTRY_POINT = ShellCEntryLib
#
# VALID_ARCHITECTURES = IA32 X64
#
[Sources]
BMP2JPG.c
[Packages]
StdLib/StdLib.dec
MdePkg/MdePkg.dec
ShellPkg/ShellPkg.dec
MdeModulePkg/MdeModulePkg.dec
[LibraryClasses]
LibC
LibStdio
DevShell
LibMath
[BuildOptions]
MSFT:*_*_*_CC_FLAGS = /wd4114 /wd4244 /wd4305
在模拟器中测试,使用方法是 bmp2jpg.efi [输入文件名] [质量0-100,从差到好]
100%质量压缩,源文件和压缩后的 JPG 基本上相同

选择 1%压缩后的结果明显变差:

完整的代码和测试数据在这里可以下载:
设计一个简单的文件传输协议
1.文件头设计
| Type | Seq | Length | Checksum |
| 2 Byte | 2 Bytes | 2 Bytes | 2 Bytes |
2.包设计
a.文件信息包
| Type | Seq | Length | Checksum | Protocol Version | FileName Length | FileSize | FileName |
| TYPE_FILE_INFO | 00 00 | 包括文件头的总长度 | 2 Bytes | 协议版本 1 byte | 1 Byte | 4 Bytes | N Bytes |
b.数据包
| Type | Seq | Length | Checksum | PayLoad |
| TYPE_DATA | NN MM | 包括文件头的总长度 | 4 Bytes | XX Byte |
c. 确认包
| Type | Seq | Length | Checksum |
| TYPE_ACK | NN MM 收到的前面包序号 | 包括文件头的总长度 | 4 Bytes |
d.结束包
| Seq | Length | Checksum | |
| TYPE_END | NN MM | 包括文件头的总长度 | 4 Bytes |
为了验证上述协议,编写一个 C 代码,在 VS2019 中编译成功,使用串口通讯。理论上串口是流通讯,并不是基于包的通讯。但是实际上我们使用的USB 转串口模块之类的,存在一个缓冲区的问题,特别是对于速度很高的情况(例如,6Mbps),收下来的数据都是先缓存在设备内部的,如果取走的速度比进入的速度快就会有数据丢失的问题。这种情况下,通讯包使用缓冲区一样大小的尺寸效率最高。
用于测试的C代码:
编译后的 exe
使用方法:
发送:
FileTransferProtocolTest.exe -s com17 FileTransferProtocolTest.pdb
接收:
FileTransferProtocolTest.exe -r com16
VS2019 编译 7-Zip的方法
1.首先在官方网站下载 Source Code:https://7-zip.org/download.html
2.打开 VS2019 X64 (注意必须 native x64 command 窗口)
3.进入 cd CPP\7zip 目录下, 运行 NMake 即可


Step to UEFI (308)UEFI Shell 下的 PPM转JPEG 程序
经过大量的测试和研究,终于找到一个可以工作的代码,能够将PPM转为JPEG 格式(通过前面的文章,我们也知道PPM和BMP 区别不大)。
项目来自 https://github.com/schiermike/jpeg-encoder/tree/master ,只有一个文件,能将PPM 转为 JPG 格式。
代码很长,这里就不列出了,有兴趣的可以直接下载研究。
需要注意的是:
1.只能除了宽度是16整数倍的图片(实际使用中,通常的处理方法是先填充到16的整数倍,处理完成之后再裁剪)

2.代码颜色上应该还有一些问题,估计是颜色排列错误

处理后的结果是

3.测试 Lenna图片

处理之后的结果

4.代码对于 PPM 格式有一些要求,文件头的参数需要用 0x0A 来进行分割,如果换成 0x0D 之类的会报错。

完整的代码和测试数据下载:
力科的 USB 抓包工具的小Bug
最近在Ch32V307上实现 USB Camera 的功能,使用之前的一个设计作为参考。结果在照抄的描述符的时候时偶然发现力科的USB T3 存在一个小Bug。
问题的描述为:描述符解析时,部分值不会反映的 Hex Value中:
最典型的是下面这个 GUID, 解析之后只有部分值,其余部分被丢掉了,如果你认为这个Field 的值为 0x32315659 只有4字节,会导致后续的描述符完全错位:

上面这种相对明显,因为 明确知道GUID应该是 16Bytes,但是下面这个就比较隐蔽,如果只将 Hex Value拷贝出来,会导致对应的结构体会不够 12 Bytes。

因此,使用工具 Dump USB设备描述符,然后编写自己的描述符时,务必数一下最后的描述符长度。
ESP32S3+MSM261S4030H0+HT513 扬声器
去年使用ESP32S3+MSM261S4030H0+HT513 做了一个扬声器的项目。具体硬件设计可以在 https://oshwhub.com/arduinoai-hao-zhe/portable-speake 看到。
在使用的时候,遇到的一个问题就是MSM261S4030H0输出是32Bits(实际只有24Bits)。最近忽然想起来查了一下是否有机会按照 16Bits输出,搜索出来的资料说的是:只要设置 I2S Master按照16Bits 取即可。因为这个输出有效数据只有 24Bits,直接截断取前面的16Bit即可。
根据这个资料编写代码如下,工作正常。
#include <Arduino.h>
#include <Wire.h>
#include <driver/i2s.h>
#define I2S_PORT1 I2S_NUM_0
#define I2S_PORT2 I2S_NUM_1
#define SAMPLE_RATE 16000
#define CHANNEL_FORMAT I2S_CHANNEL_FMT_ONLY_LEFT
//#define BITS_PER_SAMPLE I2S_BITS_PER_SAMPLE_32BIT
#define BITS_PER_SAMPLE I2S_BITS_PER_SAMPLE_16BIT
#define MIC_BCK_PIN 37
#define MIC_WS_PIN 38
#define MIC_DATA_PIN 36
#define SPK_BCK_PIN 15
#define SPK_WS_PIN 6
#define SPK_DATA_PIN 7
#define SPK_MCK_PIN 16
// HT513 音量
uint16_t Volume;
#define TOLENCE 16
#define HT513_ADDR_L 0x6c
/**
@brief ht513写寄存器
@param addr 寄存器地址
@param val 要写的值
@retval None
*/
void HT513_WriteOneByte(uint8_t addr, uint8_t val)
{
Wire.beginTransmission(HT513_ADDR_L);
Wire.write(addr);
Wire.write(val);
int ack = Wire.endTransmission(true);
Serial.print("Ack ");
Serial.println(ack, HEX);
}
/**
@brief ht513读寄存器
@param addr 寄存器地址
@retval 读取到的寄存器值
*/
uint8_t HT513_ReadOneByte(uint8_t addr)
{
uint8_t temp = 0;
Wire.beginTransmission(HT513_ADDR_L);
Wire.write(addr);
Wire.endTransmission(false);
uint8_t bytesReceived = 0;
bytesReceived = Wire.requestFrom(HT513_ADDR_L, (uint8_t)1, true);
if (bytesReceived == 1) {
temp = Wire.read();
}
else {
Serial.println("Read Error ");
}
return temp;
}
void setup() {
Serial.begin(115200);
delay(2000);
i2s_config_t MIC_i2sConfig = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = BITS_PER_SAMPLE,
.channel_format = CHANNEL_FORMAT,
.communication_format = I2S_COMM_FORMAT_I2S,
.intr_alloc_flags = 0,
.dma_buf_count = 8,
.dma_buf_len = 64,
.use_apll = false,
.tx_desc_auto_clear = false,
.fixed_mclk = 0
};
i2s_pin_config_t MIC_pinConfig = {
.bck_io_num = MIC_BCK_PIN,
.ws_io_num = MIC_WS_PIN,
.data_out_num = I2S_PIN_NO_CHANGE,
.data_in_num = MIC_DATA_PIN
};
i2s_driver_install(I2S_PORT1, &MIC_i2sConfig, 0, NULL);
i2s_set_pin(I2S_PORT1, &MIC_pinConfig);
i2s_config_t SPK_i2sConfig = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = BITS_PER_SAMPLE,
.channel_format = CHANNEL_FORMAT,
.communication_format = I2S_COMM_FORMAT_I2S,
.intr_alloc_flags = 0,
.dma_buf_count = 8,
.dma_buf_len = 64,
.use_apll = false,
.tx_desc_auto_clear = false,
.fixed_mclk = 0
};
i2s_pin_config_t SPK_pinConfig = {
.mck_io_num = SPK_MCK_PIN,
.bck_io_num = SPK_BCK_PIN,
.ws_io_num = SPK_WS_PIN,
.data_out_num = SPK_DATA_PIN,
.data_in_num = I2S_PIN_NO_CHANGE
};
i2s_driver_install(I2S_PORT2, &SPK_i2sConfig, 0, NULL);
i2s_set_pin(I2S_PORT2, &SPK_pinConfig);
analogReadResolution(9);
Wire.begin(18, 17);
// 设置 SD 为LOW
HT513_WriteOneByte(0x12, 0b11110000);
// 设置数据格式为 I2S, 16Bits
HT513_WriteOneByte(0x13, 0b00110000);
// 读取音量设置
Volume = analogRead(3);
uint8_t Vol = (Volume, 0, 511, 0x07, 0xff);
HT513_WriteOneByte(0x16, Vol);
HT513_WriteOneByte(0x15, Vol);
Serial.println(Volume, HEX);
// 调整声道
HT513_WriteOneByte(0x17, 0b10110000);
Serial.println("++++++++++++++++");
// 设置 SD 为HIGH
HT513_WriteOneByte(0x12, 0b11110100);
uint8_t Value = HT513_ReadOneByte(0x12);
Serial.println(Value, HEX);
Value = HT513_ReadOneByte(0x13);
Serial.println(Value, HEX);
Value = HT513_ReadOneByte(0x16);
Serial.println(Value, HEX);
Value = HT513_ReadOneByte(0x17);
Serial.println(Value, HEX);
Serial.print(" starting");
// dsp.beginAEC(256, 1024, 16000); // Initialize AEC with frame size, filter length, and sample rate
// dsp.enableAEC(true); // Enable AEC
}
int8_t mic[512], speaker[521], out[512];
void loop() {
size_t bytesRead = 0;
i2s_read(I2S_PORT1, &mic, sizeof(mic), &bytesRead, portMAX_DELAY);
i2s_write(I2S_PORT2, &mic, sizeof(mic), &bytesRead, portMAX_DELAY);
if (abs(analogRead(3) - Volume) > TOLENCE) {
// 读取音量设置
Volume = analogRead(3);
// 设置 SD 为LOW
HT513_WriteOneByte(0x12, 0b11110000);
uint8_t Vol = map(Volume, 0, 511, 0x07, 0xff);
HT513_WriteOneByte(0x16, Vol);
// 设置 SD 为HIGH
HT513_WriteOneByte(0x12, 0b11110100);
Serial.print(analogRead(3), HEX);
Serial.print(" ");
Serial.print(Volume, HEX);
Serial.print(" ");
Serial.println(Vol, HEX);
}
}
Step to UEFI (307)UEFI 下使用PRIu8 以及类似宏
上次的代码中碰到了C语言中的 PRIu8 ,这次专门研究了一下如何在代码中直接使用。
<inttypes.h> 是 C 标准库提供的头文件,其中有定义(例如EDK2 中的StdLib\Include\X64\machine\int_fmtio.h文件):
#define PRIu8 "u" /* uint8_t */
#define PRIu16 "u" /* uint16_t */
#define PRIu32 "u" /* uint32_t */
它们C99 标准引入的格式说明符宏。可以通过下面的这种方法进行使用:
printf("Value: %" PRIu16 "\n", value);
在 C 语言中,当你编写多个字符串字面量(例如使用双引号括起来的字符串)在代码中相邻放置时,编译器会自动将它们连接起来形成一个单独的字符串,这被称作C 语言的字符串字面量的自动连接,所以上述代码展开后就是:
printf("Value: %u \n", value);
在EDK2 中编译一个简单的代码进行验证:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <Uefi.h>
#include <Library/UefiLib.h>
#define LABZTest "LABZ"
int main(int argc, char *argv[]) {
printf("Value: " LABZTest "\n");
return 0;
}
模拟器中运行结果如下:

上述试验说明 VC2019 完全支持字符串字面量的自动连接。
所以理论上只要引用了inttypes.h 就可以使用PRIu8 这种宏定义,但是还是会遇到编译期的错误。经过研究发现 \StdLib\Include\inttypes.h 中相关引用是被注释掉的。下图中左侧是原始的代码,右侧是修改后的。完全无法理解原始代码要去掉这个引用,经过这样的修改即可正常编译和执行:

完整的测试代码:
#include <stdio.h>
#include <inttypes.h>
#include <Uefi.h>
#include <Library/UefiLib.h>
int main(int argc, char *argv[]) {
printf("Value: %" PRIu8 "\n",1234);
return 0;
}
模拟器中测试,得到了期望的结果:

完整的代码下载:
NSPGD1M006DT04 测试模块
一、项目介绍
本项目使用表压传感器NSPGD1M006DT04,制作了一个模块,能够获得当前的气压。
很多年前,如果需要制作“吹气熄灯项目”,通常只能使用声音传感器,使用获取声音的方法来判断是否进行了吹气,显而易见这种方法要受到诸多影响。现在可以使用NSPGD1M006DT04 这种传感器来来完成。
二、项目设计思路
1、芯片选型
NSPGD1M006DT04是表压传感器。表压传感器(Gauge Pressure Sensor)是一种测量相对压力的传感器,即相对于大气压力的压力值。相对的还有绝压传感器(Absolute Pressure Sensor)是一种测量相对于真空或绝对零点的压力的传感器。与表压传感器不同,绝压传感器不仅测量被测介质的压力,还考虑到环境中的气压。绝压传感器的工作原理与表压传感器类似,但其参考点是绝对真空或绝对零点而不是大气压力。
2、测试主控选型
为了方便进行测试,选择使用 Arduino 来作为主控,它可以方便的实现 I2C 通讯,同时我们还会设计一个 Arduino 的库,方便日后的使用。
3、原理图绘制
作为模块,非常简单,严格按照Datasheet设计。下图是 DataSheet 中给出的参考:

据此设计的模块电路图:

4、PCB绘制与制板

5、焊接与调试
为了便于使用,焊接了 2.54mm 排母,方便插拔测试。


三、开发过程中遇到的问题与解决方式
为了方便的进行调试,我使用了一款国产开源逻辑分析仪:章鱼哥出品的 PX logic 逻辑分析仪

这款逻辑分析仪主打高速采集,性能指标如下:
•最大32通道逻辑分析仪
•buff模式最大支持8ch@1G,总带宽8G,存储深度4Gbits
•usb3.0下stream最大2ch@1G,总带宽2G,存储深度1024Gbits
•采样usb3.0进行传输,兼容usb2.0
•输入采样阈值0-6v可调
•开源解码协议库200+,使用python进行开发,可自行开发编写
硬件核心部分采用国产器件:
- USB3.0 Phy 采用南京沁恒微的WCH569W
- FPGA采用国产FPGA 紫光同创PGL22G
其中的 Buffer 模式意思是:内部自带了缓冲,可以将采集的信号直接传输存储在内部,存储多少数据取决于当前采样数量居和板载内存的大小;Stream的意思是随时将当前采样到的数据发送到电脑上,理论上只要你的电脑性能足够,可以不停的进行采样。
在动手之前首先在面包板上动手实验(NSPGD1M006DT04这种封装非常友好,便于实验)。这里无需额外的上拉I2C 即可工作

设置采样20秒,采样率5Mhz(I2C 400KHz, 相当于10倍采样率)

Buffer 模式,2V 阈值,只要2个通道即可抓取

解码器选择 I2C

指定0 通道是 SCL, 1通道是 SDA

接下来就可以抓到结果了:

四、实现结果展示

五、关键代码及说明
读取的代码数据需要和 Datasheet 上的 Demo 完成相同:
float NSPGD1M006DT04::readPressure() {
uint32_t PData;
_i2cPort->beginTransmission(_i2cAddress);
_i2cPort->write(0x30);
_i2cPort->write(0x0A);
_i2cPort->endTransmission(true);
int number = 0;
while (number < 50) {
number++;
delay(1);
//NSPGD1M006DT04_Read_Byte(0x30,Reg30);
_i2cPort->beginTransmission(_i2cAddress);
_i2cPort->write(0x30);
_i2cPort->endTransmission(false);
_i2cPort->requestFrom(_i2cAddress, 1, true);
if (_i2cPort->read() == 0x02) {
//NSPGD1M006DT04_Read_3Byte(0x06,PData);
_i2cPort->beginTransmission(_i2cAddress);
_i2cPort->write(0x06);
_i2cPort->endTransmission(false);
_i2cPort->requestFrom(_i2cAddress, 3, true);
PData = _i2cPort->read();
PData = (PData << 8) | _i2cPort->read();
PData = (PData << 8) | _i2cPort->read();
if (PData > 8388607) {
PData = PData - 16777216;
}
return (((float)PData) / 8388607 - 0.1) / 0.13333;
}
}
return 0.0;
}
取得的数值以 float 返回给调用者。
六、难点以及解决方法
在调试过程中,我发现根据文档要求,发送指令之后,一直会出现 I2C 的 NA ,无法取得设备的数据。搜索资料也无果,无奈之下继续研究Datasheet, 忽然发现它给出的例子是多次操作的,下面这一块发送 0x0A 之后,发0x30 然后尝试50次(之前上学的时候,英文会有改错的题目,其中一种不为人知的方法是将错误通过排版的方法,放在换行或者换页的地方,看到这里忽然让我想起来被这种题目支配的恐惧)

于是,我也修改代码多次读取,然后问题就解决了。总结下来之前碰到问题的原因是:
- 为了便于调试,只尝试一次;
- 代码中间隔2秒才进行一次尝试;
- 我逻辑分析仪设置采样20秒,通常我也只会观察20秒;
修改库函数,多尝试问题就解决了。
==================================================
本项目是参考电子森林的活动项目,原始链接在 https://www.eetree.cn/project/4719
在线取得 EFI 文件中文本的工具
在 https://convert.guru/converter 提供了一个在线工具,能够解析出来 EFI 文件中的文本字符串。
用法非常简单,把EFI 文件拖拽上去即可解析,比如,我使用手边的 EFI 测试如下:

估计这个工具是用来方便进行EFI 破解的吧?
给 7Z 增加解压后打开目录的功能
修改 7Zip CPP\7zip\UI\GUI\ExtractGUI.cpp ExtractGUI() 函数,在末尾增加一段代码:
extracter.ArchivePaths = &archivePaths;
extracter.ArchivePathsFull = &archivePathsFull;
extracter.WildcardCensor = &wildcardCensor;
extracter.Options = &options;
#ifndef Z7_SFX
extracter.HashBundle = hb;
#endif
extracter.IconID = IDI_ICON;
RINOK(extracter.Create(title, hwndParent))
messageWasDisplayed = extracter.ThreadFinishedOK && extracter.MessagesDisplayed;
//LAB-Z_Debug_Start
if (extracter.Result == S_OK && !options.TestMode && !options.OutputDir.IsEmpty())
{
FString outputDir = options.OutputDir;
NName::NormalizeDirPathPrefix(outputDir);
ShellExecute(NULL, L"explore", fs2us(outputDir), NULL, NULL, SW_SHOWNORMAL);
}
//LAB-Z_Debug_End
return extracter.Result;
}
重新编译,之后将编译生成的 7zG.exe 拷贝覆盖到之前的 7z 安装目录下,使用右键解压的时候就能自动打开解压目录了。
修改后的代码:
基于26.01 源代码修改后编译生成的 7zG.exe: