2025年2月更新,Step to UEFI 文章索引:
Step to UEFI (299)替换ReadKeyStroke的实验
这次的代码是根据之前的文章【参考1】修改而来,将当前系统中 Simple Simple Input Protocol 中的ReadKeyStroke()函数替换为自定的。为了便于验证,如果当前返回了 ‘z’ ,那么会将这个替换为’a’。
完整的代码如下:
#include <Library/DebugLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiDriverEntryPoint.h>
#include <Library/UefiLib.h>
#include <PiDxe.h>
#include <Protocol/SimpleFileSystem.h>
extern EFI_SYSTEM_TABLE *gST;
EFI_GUID gEfiSimpleTextInputProtocolGuid = {
0x387477c1,
0x69c7,
0x11d2,
{0x8e, 0x39, 0x0, 0xa0, 0xc9, 0x69, 0x72, 0x3b}};
// EFI_FILE_PROTOCOL *Root;
EFI_SIMPLE_TEXT_INPUT_PROTOCOL *SimpleInput;
EFI_INPUT_READ_KEY OldReadKeyStroke;
// This one will replace the Read function of Read in Simple File System
EFI_STATUS
EFIAPI
MyReadKeyStroke(IN EFI_SIMPLE_TEXT_INPUT_PROTOCOL *This,
OUT EFI_INPUT_KEY *Key) {
//EFI_INPUT_KEY IKey;
EFI_TPL OldTpl;
EFI_STATUS Status;
//
// Enter critical section
//
OldTpl = gBS->RaiseTPL(TPL_NOTIFY);
Status = (*OldReadKeyStroke)(SimpleInput, Key);
if (Key->UnicodeChar==0x7A) {
Key->UnicodeChar=0x61;
}
//
// Leave critical section and return
//
gBS->RestoreTPL(OldTpl);
return Status;
}
EFI_STATUS
EFIAPI
MyEntryPoint(IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable) {
EFI_STATUS Status;
// Look for one Simple Simple Input Protocol
Status =
gBS->LocateProtocol(&gEfiSimpleTextInputProtocolGuid, NULL, &SimpleInput);
if (EFI_ERROR(Status)) {
gST->ConOut->OutputString(gST->ConOut,
L"Can't find Simple Input PROTOCOL\n");
return Status;
}
OldReadKeyStroke = SimpleInput->ReadKeyStroke;
SimpleInput->ReadKeyStroke = MyReadKeyStroke;
return Status;
}
需要注意的上述代码是驱动:
1.在 MdeModulePkg 下面编译通过,在\MdeModulePkg\MdeModulePkg.dsc 添加如下:
MdeModulePkg/Universal/RegularExpressionDxe/RegularExpressionDxe.inf
MdeModulePkg/Universal/SmmCommunicationBufferDxe/SmmCommunicationBufferDxe.inf
MdeModulePkg/Universal/Disk/RamDiskDxe/RamDiskDxe.inf
MdeModulePkg/KSTest/KSTest.inf
[Components.X64]
MdeModulePkg/Universal/CapsulePei/CapsuleX64.inf
[BuildOptions]
2.编译使用
build -a X64 -p MdeModulePkg\MdeModulePkg.dsc -t VS2019
3.运行时使用如下命令加载
load kst.efi
参考:
1.https://www.lab-z.com/stu163rp/ 替换已经存在Protocol中函数的实验
EasyX 用点画椭圆
椭圆的参数方程:

源代码:
#include <graphics.h> // EasyX图形库头文件
#include <conio.h> // 用于_getch()
#include <math.h>
#define a 100
#define b 50
int main()
{
int x,y;
// 初始化640x480像素的图形窗口
initgraph(640, 480);
for (int i = 0; i < 360; i++) {
x = 320 + a * cos(i * 3.1415 / 180); // sin 用弧度做参数
y= 240+ b * sin(i * 3.1415 / 180); // cos 用弧度做参数
putpixel(x, y, RED);
}
// 保持窗口显示
_getch();
// 关闭图形窗口
closegraph();
return 0;
}
运行结果:

和之前画圆类似,直接用点的方法计算:
#include <graphics.h> // EasyX图形库头文件
#include <conio.h> // 用于_getch()
#include <math.h>
#include <stdio.h>
#define Xcenter 320
#define Ycenter 240
// 长轴
#define A 50
// 短轴
#define B 30
// 焦点F1坐标
#define Xf1 (Xcenter-(int)(sqrt(A*A-B*B)))
#define Yf1 Ycenter
// 焦点F2坐标
#define Xf2 (Xcenter+(int)(sqrt(A*A-B*B)))
#define Yf2 Ycenter
// 计算 (x,y) 到焦点F1和F2的距离之和
double CalculateDistance(int x, int y)
{
double f1 = sqrt((x - Xf1) * (x - Xf1) + (y - Yf1) * (y - Yf1));
double f2 = sqrt((x - Xf2) * (x - Xf2) + (y - Yf2) * (y - Yf2));
return (f1+f2);
}
// 找到下一个点位
// 输入当前点位坐标 (Xcurrent,Ycurrent)
// 前一个点位坐标 (Xlast,Ylast
void FindNextPoint(int Xcurrent, int Ycurrent, int Xlast, int Ylast, int *Xnext, int *Ynext)
{
double gap= 1000000000.0;
double tmp;
// 左
if ((Xcurrent - 1 != Xlast) || (Ycurrent != Ylast)) {
tmp = fabs(CalculateDistance(Xcurrent - 1, Ycurrent) - 2 * A);
if (gap > tmp) {
gap = fabs(CalculateDistance(Xcurrent - 1, Ycurrent) - 2 * A);
*Xnext = Xcurrent - 1;
*Ynext = Ycurrent;
}
}
// 左上
if ((Xcurrent-1 != Xlast) || (Ycurrent-1!= Ylast)) {
tmp = fabs(CalculateDistance(Xcurrent - 1, Ycurrent - 1) - 2 * A);
if (gap > tmp) {
gap = fabs(CalculateDistance(Xcurrent - 1, Ycurrent-1) - 2 * A);
*Xnext = Xcurrent - 1;
*Ynext = Ycurrent-1;
}
}
// 上
if ((Xcurrent!= Xlast) || (Ycurrent - 1 != Ylast)) {
tmp = fabs(CalculateDistance(Xcurrent, Ycurrent - 1) - 2 * A);
if (gap > tmp) {
gap = fabs(CalculateDistance(Xcurrent, Ycurrent - 1) - 2 * A);
*Xnext = Xcurrent;
*Ynext = Ycurrent - 1;
}
}
// 右上
if ((Xcurrent+1 != Xlast) || (Ycurrent - 1 != Ylast)) {
if (gap > fabs(CalculateDistance(Xcurrent+1, Ycurrent - 1) - 2 * A)) {
gap = fabs(CalculateDistance(Xcurrent+1, Ycurrent - 1) - 2 * A);
*Xnext = Xcurrent+1;
*Ynext = Ycurrent - 1;
}
}
// 右
if ((Xcurrent + 1 != Xlast) || (Ycurrent != Ylast)) {
if (gap > fabs(CalculateDistance(Xcurrent + 1, Ycurrent) - 2 * A)) {
gap = fabs(CalculateDistance(Xcurrent + 1, Ycurrent) - 2 * A);
*Xnext = Xcurrent + 1;
*Ynext = Ycurrent;
}
}
// 右下
if ((Xcurrent + 1 != Xlast) || (Ycurrent+1 != Ylast)) {
if (gap > fabs(CalculateDistance(Xcurrent + 1, Ycurrent+1) - 2 * A)) {
gap = fabs(CalculateDistance(Xcurrent + 1, Ycurrent+1) - 2 * A);
*Xnext = Xcurrent + 1;
*Ynext = Ycurrent + 1;
}
}
// 下
if ((Xcurrent != Xlast) || (Ycurrent +1!= Ylast)) {
if (gap > fabs(CalculateDistance(Xcurrent, Ycurrent+1) - 2 * A)) {
gap = fabs(CalculateDistance(Xcurrent, Ycurrent+1) - 2 * A);
*Xnext = Xcurrent;
*Ynext = Ycurrent+1;
}
}
// 左下
if ((Xcurrent-1 != Xlast) || (Ycurrent + 1 != Ylast)) {
if (gap > fabs(CalculateDistance(Xcurrent-1, Ycurrent + 1) - 2 * A)) {
gap = fabs(CalculateDistance(Xcurrent-1, Ycurrent + 1) - 2 * A);
*Xnext = Xcurrent-1;
*Ynext = Ycurrent + 1;
}
}
}
int main()
{
int x=0, y= 0;
int Xlast = Xcenter - A, Ylast = Ycenter;
int Xcurrent = Xcenter - A, Ycurrent = Ycenter;
// 初始化640x480像素的图形窗口
initgraph(640, 480);
for (int i = 0; i < 255; i++) {
FindNextPoint(Xcurrent, Ycurrent,Xlast,Ylast,&x,&y);
//printf("%d %d\n", x- Xcenter, y-Ycenter);
putpixel(Xcurrent, Ycurrent, RED);
Xlast = Xcurrent;
Ylast = Ycurrent;
Xcurrent = x;
Ycurrent = y;
if ((i != 0) && (Xcurrent == Xcenter - A) && (Ycurrent == Ycenter)) {
printf("-->%d \n",i);
}
}
// 保持窗口显示
_getch();
// 关闭图形窗口
closegraph();
return 0;
}
之前提到计算周长的问题,椭圆周长公式【参考】:L=T(r+R),其中的 T 是短轴长轴的比例。比如,这里我们 A=50, B=30 因此,T=3.190874858, 计算结果是255.2,上述代码运行之后会输出 “–>231” 意思是在 234 的时候到了起始位置,结果和预期有可比性。
参考:
Step to memory 005 延迟和流程的演变
原文在 https://www.bit-tech.net/reviews/tech/memory/the_secrets_of_pc_memory_part_1/8/
延迟
DDR 的每一代发展都提供了更快的数据速率和更高的容量,但其代价是稳定性和信号准确性的下降。因此,每一代 DDR 都需要适应更高的频率和更高的信号延迟。
延迟可以理解为为时间上的暂停或延迟。当 DDR 信号命令发生变化时,内存子系统需要在不同的内存命令之间暂停。这就像一列火车驶入车站,然后停下来让乘客上下车。
延迟有很多种类型。CAS 延迟通常被认为是最重要的延迟之一,然而随着 DDR 内存的更新换代,它本身的重要性逐渐降低,而多个延迟值的组合则更为重要。许多内存模块将 CAS 延迟表示为“CL”或简称为“C”。例如,CAS 延迟为 3 个时钟周期的内存模块通常被标记为CL3 或 C3。相关知识将在后续文章中讨论。

演化过程 在更高数据吞吐量需求的推动下,DDR 相关的各种技术经历了一系列的演进,同时尽可能保持经济性。
提高内存数据传输频率面临的两大挑战是信号噪声水平和时序精度。这通常被称为有效数据窗口 (Valid Data Window , DVW)。有效数据窗口有时也称为数据有效窗口(Data-Valid Window,DVW)或简称为“眼图”。它是决定信号可靠性主要因素。

为了降低信号反射,。例如:DDR1 采用主板上添加终端电阻方法,DDR2 则采用片上终端电阻 (On-Die Termination,ODT) 方法。DDR3 进一步扩展了 ODT 技术,允许根据情况动态调整 ODT 值。结合各种信号校准技术,可以合理地管理数据完整性,从而实现更快的传输速率。所有内存模块和主板都需要在设计和后期生产过程中进行信号准确性测试和验证,以确保各种内置校准方案正常运行。
这里提到的信号反射指的是高速信号中的反射问题【参考1】https://blog.csdn.net/hs977986979/article/details/142762703
在硬件电路中,高频信号的反射是一个非常重要的现象。当电磁波在传输线上传播时,如果遇到阻抗不连续点(如传输线的末端、拐角、过孔、元件引脚、线宽变化等),就会发生反射。反射波的大小和方向取决于入射波的幅度、相位以及阻抗不连续点的性质。
反射现象会导致信号轮廓失真,产生过冲、欠冲和振荡等问题。这些问题会影响电路的性能和稳定性。为了减小反射,通常需要在传输线的末端添加适当的终端匹配电阻,以确保信号的完整传输。
为了实现更高的效率和更低的散热,需要降低内存电压。数据中心运营商、台式机和笔记本电脑消费者对低功耗计算机越来越感兴趣,原因包括更环保、更长的使用时间以及总体运营成本的降低。
现代数据中心使用大量空调来维持运行。例如,卢卡斯影业 (LucasFilm) 的数据中心在 32 台空调机组中使用了 25 吨冷却剂来维持其系统的运行。功率效率通常以每瓦性能来计算,因此在保持相同性能的情况下,瓦数的降低对行业来说都是利好消息。
计算机内存系统如果没有多年的预先规划和全行业协商,就不会发生重大的范式转变。成员们会将一次又一次的修订提交给像 JEDEC 这样的设计管理机构,然后由一个合作伙伴委员会监督整个设计和批准过程。
制造过程中涉及的制造和测试设备成本高昂,这极大地阻碍了变革,因为自动测试设备 (Automatic Test Equipment,ATE) 价格极其昂贵,通常每台设备的成本高达数百万美元。MOSAID Systems 的 Brad Snoulten 认为,主要挑战有两个:
- 内存制造商无力购买或更换价值数百万美元的测试仪来满足不断增长的生产或工程测试需求。
- 内存ATE供应商面临着设计经济实惠且能有效抵御长期淘汰的解决方案的挑战。内存裕度的不断下降和设备复杂性的不断增加加剧了这些问题。
进步是一个渐进的演变过程,而不会出现跳跃式的进步。
第九章 GDDR,QDR 和 XDR
如果您在过去三年内购买组装一个台式机,它很可能至少有一个 PCI Express 图形扩展接口。这些显卡拥有一种专用的 DDR,即图形 DDR (GDDR) 内存,容量从最小的几十兆字节到超过上千兆字节甚至更多。
GDDR通常用于对带宽要求极高的高性能显卡。需要主意它的架构与 DDR 截然不同, 在JEDEC 的规范中GDDR 与 DDR 标准分属不同规范。
最新的 GDDR 技术已发展到第五代,简称为 GDDR5。不同代之间的主要区别在于性能和带宽。与 DDR 相比,GDDR 具有更高的性能,但在制造成本和功耗方面也明显更高。GDDR3的工作电压为 2.0V,而 DDR3 的工作电压为 1.5V,额外的电压有助于 GDDR 更快地运行,但也使其更容易泄漏电流,从而产生更多热量。这与大型图形运算核心组合意味着显卡产生的热量通常比主板中的 CPU 和内存加起来还要多。

Nvidia 8800GT 上的奇梦达 GDDR3
GDDR 具有较宽的数据接口和更大的帧缓冲区。与 DDR 设备相比,显卡等 GDDR 设备的单位容量较低;这会导致功耗更高且价格更高。增加设备的 GDDR 显存容量会相应增加功耗和设备价格。
QDR和XDR
四倍数据速率 (QDR) 内存系统始于 1999 年,由Cypress 导体公司、IDT 和 NEC 共同开发。此后,包括美光科技、瑞萨电子、三星电子和日立在内的多家公司都以某种形式参与其中。
我们预计, QDR 内存取代 DDR 的可能性不高,最重要的原因是QDR无法实现低成本的量产。其次,此举相当于内存技术的一次重大转向,出于经济性和实用性的考虑,很多制造商对此非常反对。
虽然 QDR 的数据频率和有效数据窗口显著提高,并具有超低延迟,但其内存容量相对于 DDR 而言相对较低。当 DDR3 达到每模块 8GB 时,QDR3 内存容量标准仍然以 MB 为单位。QDR 架构是特地为高性能通信应用而设计。
Lattice半导体公司表示,与 QDR 相比,DDR 技术存在以下缺点:
- 写入和读取共享一条双向数据总线,因此总带宽与 QDR 架构相比减少了一半。这在写入与读取比例接近 1:1 的情况下意义重大。
- 刷新需要中断数据传输。
- 访问延迟相对较高。
- 需要在上电后进行初始化,并在访问之前/之后激活/预充电行(内存接口简化了这一点)。
值得注意的是,目前市场上已经有了QDR内存甚至ODR内存(Octal-Data Rate)。索尼 PlayStation 3 采用 Rambus 的 XDR(极限数据速率)设计,该设计能够在每个时钟周期发送 8 位数据,而 DDR 只能在每个时钟周期发送 2 位数据。三星、奇梦达、尔必达、IBM、东芝、AMD 等公司目前都采用 XDR 设计。这些内存系统价格极其昂贵,而且不像 DDR 那样普及。
有一种罕见的 DDR2 类型,称为 DDR2+(或增强型 DDR2),其核心频率可达 333MHz,而 DDR2 标准频率为 200MHz。DDR2+ 也是由 QDR 联盟联合开发的。
下次我们将研究移动 DDR 和底层 DDR 技术,如温度补偿自刷新、部分阵列自刷新、深度断电和时钟停止模式以及 DRAM 封装和堆叠技术。
======================================================
讲个好玩的事情,三星内存的“反周期策略”:
三星充分利用了存储器行业的强周期特点,在价格下跌、生产过剩、其他企业削减投资的时候,逆势疯狂扩产,通过大规模生产进一步下杀产品价格,从而逼竞争对手退出市场甚至直接破产,世人称之为“反周期定律”。
在存储器这个领域,三星一共祭出过三次“反周期定律”,前两次分别发生在80年代中期和90年代初,让三星从零开始,做到了存储器老大的位置。但三星显然觉得玩的还不够大,于是在2008年金融危机前后,第三次举起了“反周期”屠刀。2007 年初,微软推出了狂吃内存的Vista操作系统,DRAM厂商判断内存需求会大增,于是纷纷上产能,结果Vista 销量不及预期,DRAM 供过于求价格狂跌,加上08 年金融危机的雪上加霜,DRAM 颗粒价格从2.25 美金雪崩至0.31 美金。
就在此时,三星做出令人瞠目结舌的动作:将2007 年三星电子总利润的118%投入DRAM 扩张业务,故意加剧行业亏损,给艰难度日的对手们,加上最后一根稻草。效果是显著的。DRAM价格一路飞流直下,08年中跌破了现金成本,08年底更是跌破了材料成本。2009年初,第三名德系厂商奇梦达首先撑不住,宣布破产,欧洲大陆的内存玩家就此消失。2012年初,第五名尔必达宣布破产,曾经占据DRAM市场50%以上份额的日本,也输掉了最后一张牌。在尔必达宣布破产当晚,京畿道的三星总部彻夜通明,次日股价大涨,全世界都知道韩国人这次又赢了。至此,DRAM领域最终只剩三个玩家:三星、海力士和镁光。尔必达破产后的烂摊子,在2013年被换了新CEO的镁光以20多亿美金的价格打包收走。20亿美金实在是个跳楼价,5年之后,镁光市值从不到100亿美元涨到460亿,20亿美元差不多是它市值一天的振幅。
上述来自雪球 作者:潇潇小鱼 链接:https://xueqiu.com/2691707350/179978325
EasyX 用点画圆
圆形的参数方程如下:

根据这个,编写 EasyX 代码如下:
#include <graphics.h> // EasyX图形库头文件
#include <conio.h> // 用于_getch()
#include <math.h>
#define RAD 100
int main()
{
int x,y;
// 初始化640x480像素的图形窗口
initgraph(640, 480);
for (int i = 0; i < 360; i++) {
x = 320 + RAD * sin(i * 3.1415 / 180); // sin 用弧度做参数
y= 240+ RAD * cos(i * 3.1415 / 180); // cos 用弧度做参数
putpixel(x, y, RED);
}
// 保持窗口显示
_getch();
// 关闭图形窗口
closegraph();
return 0;
}
运行结果:

此外,这里再给出一个奇怪/低效的算法。基于如下限制:所有的点在八个方向上只存在两个点与之相近(换句话,一个像素的线绘制出来的圆形),基于此限制,设计算法如下:
- 给出起始点坐标(Xorg,Yorg);
- 生成8个方向的坐标,计算这些点位和圆心的距离,然后选择最接近半径的作为下一个点的坐标;
- 重复上述动作计算出所有的点(不过我还没有想明白怎么通过公式计算出点的数量)
#include <graphics.h> // EasyX图形库头文件
#include <conio.h> // 用于_getch()
#include <math.h>
//#include <stdio.h>
#define Xcenter 320
#define Ycenter 240
#define RAD 100
// 计算 (x,y) 到圆心的距离
double CalculateDistance(int x, int y)
{
return (sqrt((x - Xcenter) * (x - Xcenter) + (y - Ycenter) * (y - Ycenter)));
}
// 找到下一个点位
// 输入当前点位坐标 (Xcurrent,Ycurrent)
// 前一个点位坐标 (Xlast,Ylast
void FindNextPoint(int Xcurrent, int Ycurrent, int Xlast, int Ylast, int *Xnext, int *Ynext)
{
double gap= 1000000000.0;
double tmp;
// 左
if ((Xcurrent - 1 != Xlast) || (Ycurrent != Ylast)) {
tmp = CalculateDistance(Xcurrent - 1, Ycurrent);
tmp = fabs(tmp - RAD);
if (gap > tmp) {
gap = fabs(CalculateDistance(Xcurrent - 1, Ycurrent) - RAD);
*Xnext = Xcurrent - 1;
*Ynext = Ycurrent;
}
}
// 左上
if ((Xcurrent-1 != Xlast) || (Ycurrent-1!= Ylast)) {
if (gap > fabs(CalculateDistance(Xcurrent - 1, Ycurrent-1) - RAD)) {
gap = fabs(CalculateDistance(Xcurrent - 1, Ycurrent-1) - RAD);
*Xnext = Xcurrent - 1;
*Ynext = Ycurrent-1;
}
}
// 上
if ((Xcurrent!= Xlast) || (Ycurrent - 1 != Ylast)) {
if (gap > fabs(CalculateDistance(Xcurrent, Ycurrent - 1) - RAD)) {
gap = fabs(CalculateDistance(Xcurrent, Ycurrent - 1) - RAD);
*Xnext = Xcurrent;
*Ynext = Ycurrent - 1;
}
}
// 右上
if ((Xcurrent+1 != Xlast) || (Ycurrent - 1 != Ylast)) {
if (gap > fabs(CalculateDistance(Xcurrent+1, Ycurrent - 1) - RAD)) {
gap = fabs(CalculateDistance(Xcurrent+1, Ycurrent - 1) - RAD);
*Xnext = Xcurrent+1;
*Ynext = Ycurrent - 1;
}
}
// 右
if ((Xcurrent + 1 != Xlast) || (Ycurrent != Ylast)) {
if (gap > fabs(CalculateDistance(Xcurrent + 1, Ycurrent) - RAD)) {
gap = fabs(CalculateDistance(Xcurrent + 1, Ycurrent) - RAD);
*Xnext = Xcurrent + 1;
*Ynext = Ycurrent;
}
}
// 右下
if ((Xcurrent + 1 != Xlast) || (Ycurrent+1 != Ylast)) {
if (gap > fabs(CalculateDistance(Xcurrent + 1, Ycurrent+1) - RAD)) {
gap = fabs(CalculateDistance(Xcurrent + 1, Ycurrent+1) - RAD);
*Xnext = Xcurrent + 1;
*Ynext = Ycurrent + 1;
}
}
// 下
if ((Xcurrent != Xlast) || (Ycurrent +1!= Ylast)) {
if (gap > fabs(CalculateDistance(Xcurrent, Ycurrent+1) - RAD)) {
gap = fabs(CalculateDistance(Xcurrent, Ycurrent+1) - RAD);
*Xnext = Xcurrent;
*Ynext = Ycurrent+1;
}
}
// 左下
if ((Xcurrent-1 != Xlast) || (Ycurrent + 1 != Ylast)) {
if (gap > fabs(CalculateDistance(Xcurrent-1, Ycurrent + 1) - RAD)) {
gap = fabs(CalculateDistance(Xcurrent-1, Ycurrent + 1) - RAD);
*Xnext = Xcurrent-1;
*Ynext = Ycurrent + 1;
}
}
}
int main()
{
int x=0, y= 0;
int Xlast = Xcenter - RAD, Ylast = Ycenter;
int Xcurrent = Xcenter - RAD, Ycurrent = Ycenter;
// 初始化640x480像素的图形窗口
initgraph(640, 480);
for (int i = 0; i < 800; i++) {
FindNextPoint(Xcurrent, Ycurrent,Xlast,Ylast,&x,&y);
//printf("%d %d\n", x, y);
putpixel(Xcurrent, Ycurrent, RED);
Xlast = Xcurrent;
Ylast = Ycurrent;
Xcurrent = x;
Ycurrent = y;
}
// 保持窗口显示
_getch();
// 关闭图形窗口
closegraph();
return 0;
}
Step to memory 004 单通道和双通道
继续 《PC 内存的秘密 第一部分》原文在 https://www.bit-tech.net/reviews/tech/memory/the_secrets_of_pc_memory_part_1/6/
有几个独特但重要的速度概念需要牢记。它们是 DRAM 核心频率、输入输出 (IO) 缓冲区频率、内存总线频率和数据频率。它们用于描述内存系统不同部件的性能水平。
所有计算机内存模块都以数据频率为标称值。例如,DDR2-800 中的“800”数字描述了模块以 800MHz频率搬运数据的能力。另一方面,IO 缓冲区和总线频率将以 400MHz 运行,而 DRAM 核心频率仅以 200MHz 运行。
频率 (MHz) 和数据吞吐量 (Mbps) 之间的关系很简单。一个信号是 1 位数据:0 或 1。下图中的圆形黑色“操作点”。800MHz 数据频率意味着它将以每秒 8 亿次的速度发送 1 位数据,因此两者的乘积等于每秒 800 兆比特 (Mbps)。 (注:请记住,位和字节是不同的:8 位 = 1 字节,因此之前的 800Mbps 仅相当于 100MBps 或每秒兆字节)。
在给定数据频率的情况下,实际或有效数据吞吐量低于预期。例如,800MHz 的 DDR2 不会将该频率全部用于数据流。相反,命令和控制信号会占用部分数据。这有点像购买 160GB 硬盘,格式化后,只有 149GB 可用于数据。这是因为驱动器的一部分被系统级信息(如主引导记录 MBR 和分区表)占用。
对于一些爱好者和完美主义者来说,这是内存超频很好的理由,以使数据吞吐量更接近或超过 800MHz 数据流。使用指定为 1066MHz 的 DDR2 模块可以更好地实现这一点。

双通道和单通道模式
DDR 的另一项重大创新是能够使用双通道,而不是传统的单通道内存总线。这种设计极大地提高了内存性能。
在对称双通道芯片组上,将两个内存模块放在相同颜色的插槽中将自动为用户提供双通道性能,但是在 4 插槽主板上使用 3 个内存模块将使主板切换回单通道模式。非对称双通道芯片组能够使用 3 个 DIMM 在双通道模式下运行,从而始终有效地为用户提供 128 位内存性能。
当前基于台式机的 DDR 内存技术无法支持每通道超过 2 个 DIMM,但基于服务器和工作站的 FB-DIMM(全缓冲 DIMM)内存控制器(用于英特尔 CPU)设计时考虑了每通道 8 个 DIMM。这些高端计算机通常配备四通道配置,需要至少四个 DIMM 才能使用它。


每个标准台式机内存模块支持 64 位数据总线,而基于服务器的内存模块每通道使用 72 位数据总线:额外的 8 位用于纠错码 (ECC)。由于增加了额外的性能和纠错特性的复杂性,R-DIMM 和 FB-DIMM 比台式机使用的内存贵得多。
CPU 和带宽增长
内存系统与 CPU 的演进有着直接的关系。随着更强大的处理器进入市场,需要更多的内存带宽来跟上 CPU 的处理速度。较慢的内存系统无法向快速的 CPU 提供足够的数据,这会导致处理器花费更多的时间在等待更多数据。内存系统的目标是在尽可能短的时间内为不同的任务存储和检索大量数据。如果没有同样快速的内存系统,CPU 将无法充分发挥效能导致效率低下。
下图说明了这一现象。摩尔定律描述 CPU 的处理能力每 18 个月翻一番。英特尔核心团队负责人 David “Dadi” Perlmutter 将其归类为“经济性和技术进步的简单关系”。计算机的效率在很大程度上依赖于内存系统来跟上 CPU 方面的改进。

单核 CPU 过去只是通过提高处理频率来提升性能。2004-2005 年,一种新的台式机 CPU 设计将处理器改进的动态从原始速度的单一因素转变为两个因素:处理速度以及核心数量。
此后的 CPU 性能提升依赖于核心频率的改进、处理器封装内的核心数量、具有更先进的预测和预取优化的缓存技术以及改进的总线效率以避免瓶颈。用于内存访问的数据预测和预取算法对处理器效率有重大影响。其他因素包括 L1、L2(和 L3)缓存的使用方式及其相关算法和访问分配。
第七章 EMI和内存控制器
由于内存速度从未真正能够跟上 CPU 性能,内存系统设计人员一直在使用多种手段改进性能。出于成本考虑原因,设计的主要目标一直是尽可能降低制造成本。
创新的设计人员没有选择提升内存的原始速度,而是通过并行访问的方法来提升整体速度。这些创造性的技术包括:双倍数据速率技术(DDR)、双通道或四通道模式(Dual or Quad Channel Modes)以及预取技术(Pre-fetching )等等。
需要特别主意的是,DDR DRAM 在技术工程角度存在成本限制,核心频率限制在 200MHz时整体成本最优。提升芯片外部总线传输频率相较于提升 DRAM 核心频率成本上要划算的多。
而为了达到上述目标,主板和内存模块必须在精确的信号管理和电磁干扰(EMI)控制机制方面取得设计和制造突破。因此,内存改进不仅仅是 DRAM 设计人员考虑的问题,也是从 CPU 架构师、芯片组和 DRAM 设计人员到主板和内存模块制造商的全行业协调努力的结果。
内存控制器
内存控制器的职责是对内存模块发送命令、管理内存模块和路由信号到内存模块。每个 DRAM 通常有四个或八个内存组,访问方式与 Microsoft Excel 工作表类似:有行和列。每当 CPU 需要读取或存储数据时,它都会通过内存控制器将信息发送到 RAM。
英特尔今年已经以 P35 和 X38 芯片组的形式发布了“Bearlake”3 系列家族下的第一代 DDR3 主板。Nvidia 和后来的 AMD 为其自己的 CPU 推出的未来芯片组也将在 2008 年支持 DDR3。当前的 DDR2/DDR3“组合”主板能够支持现有的 DDR2-800 或新的 DDR3-1066/1333 内存模块,但不能同时支持两个标准。DDR2 和 DDR3 使用不同的内存插槽,因为它们具有不同的模块键槽位置。

Intel 从Nehalem架构(45nm)开始整合了内存控制器(Integrated Memory Controller,IMC)。现在市面上的Intel处理器都已经整合了IMC。
将内存集成到 CPU 封装中的直接经济效益是降低该特定平台的主板制造成本。技术效益是减短CPU 对内存传输数据路径。集成内存控制器消除了对物理前端总线 (FSB) 的需求。无需独立的内存控制器芯片,主板制造商的设计和测试过程变得更简单 : EMI 问题更少,整个系统本耗设更低。理论上,这种设计消除了前端总线(FSB)带来的带宽瓶颈,可以显著提高内存性能。
集成内存控制器的缺点是它占用了处理器芯片上的空间,而这些空间原本可以用于更大的 L1、L2 和 L3 缓存。此外,任何直接内存访问 (DMA) 都必须经由 CPU 才能进入内存,这会为需要快速访内存的其他组件(如显卡)带来额外的延迟。
讲一个好玩的,我入行的时候是在昆山的微兴做台式机主板,偶然发现 Intel 的内存兼容性要比 AMD 好多了。发现的原因是当前公司研发有一批DQA 人员负责测试。有时候项目到了后期,实在没有什么好测试的他们就会拿出来内存“扫”一圈。Intel 通常没啥问题,比如,上一次是两三个型号有问题,这一次仍然是这些有问题。但是 AMD 就麻烦了,这次扫出来的和上一次有问题的型号往往不同,让人无比头大。
后来稍微有点经验,对于有问题的内存通过 SPD 信息判断,然后升高电压来解决。万幸的是这种方法对于大部分问题都有效果。
不使用第三方库实现 IIS 麦克风转HTTP
这段代码展示了不使用第三方库实现在一个 HTTP Server, 然后电脑可以通过浏览器打开 http://ip/stream 即可听到 IIS 麦克风拾取到的声音。实验使用MSM261S4030H0的 IIS 音频传感器。
#include <WiFi.h>
#include <WebServer.h>
#include <driver/i2s.h>
// WiFi配置
const char* ssid = "YOURSSID";
const char* password = "YOURPASSWORD";
// I2S配置
#define I2S_MIC_WS 37
#define I2S_MIC_SD 48
#define I2S_MIC_SCK 45
#define SAMPLE_RATE 16000
#define BITS_PER_SAMPLE 32
#define BUFFER_SIZE 1024
WebServer server(80);
int32_t audioBuffer[BUFFER_SIZE];
void setup() {
Serial.begin(2000000);
// 初始化I2S
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 8,
.dma_buf_len = BUFFER_SIZE,
.use_apll = false
};
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_MIC_SCK,
.ws_io_num = I2S_MIC_WS,
.data_out_num = I2S_PIN_NO_CHANGE,
.data_in_num = I2S_MIC_SD
};
i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
i2s_set_pin(I2S_NUM_0, &pin_config);
// 连接WiFi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected");
Serial.println("IP address: " + WiFi.localIP().toString());
// 设置HTTP路由
server.on("/stream", HTTP_GET, handleAudioStream);
server.begin();
}
void loop() {
server.handleClient();
// 持续读取音频数据
size_t bytesRead;
i2s_read(I2S_NUM_0, &audioBuffer, BUFFER_SIZE * sizeof(int32_t), &bytesRead, portMAX_DELAY);
}
void handleAudioStream() {
// 生成WAV文件头
uint8_t wavHeader[44];
generateWavHeader(wavHeader, SAMPLE_RATE, 32);
// 发送HTTP头
server.setContentLength(CONTENT_LENGTH_UNKNOWN);
server.send(200, "audio/wav", "");
// 先发送WAV头
server.sendContent((const char*)wavHeader, sizeof(wavHeader));
// 持续发送音频数据
while(server.client().connected()) {
size_t bytesRead;
i2s_read(I2S_NUM_0, &audioBuffer, BUFFER_SIZE*4, &bytesRead, portMAX_DELAY);
server.sendContent((const char*)audioBuffer, bytesRead);
}
}
void generateWavHeader(uint8_t* header, uint32_t sampleRate, uint32_t bitDepth) {
// RIFF块标识
header[0] = 'R'; header[1] = 'I'; header[2] = 'F'; header[3] = 'F';
// 文件总大小占位(后续更新)
header[4] = 0; header[5] = 0; header[6] = 0; header[7] = 0;
// WAVE格式标识
header[8] = 'W'; header[9] = 'A'; header[10] = 'V'; header[11] = 'E';
// fmt子块标识
header[12] = 'f'; header[13] = 'm'; header[14] = 't'; header[15] = ' ';
// fmt块大小(16字节)
header[16] = 16; header[17] = 0; header[18] = 0; header[19] = 0;
// 音频格式(1=PCM)
header[20] = 1; header[21] = 0;
// 声道数(1=单声道)
header[22] = 1; header[23] = 0;
// 采样率
header[24] = sampleRate & 0xFF;
header[25] = (sampleRate >> 8) & 0xFF;
header[26] = (sampleRate >> 16) & 0xFF;
header[27] = (sampleRate >> 24) & 0xFF;
// 字节率 = 采样率 * 声道数 * 位深度/8
uint32_t byteRate = sampleRate * (bitDepth/8);
header[28] = byteRate & 0xFF;
header[29] = (byteRate >> 8) & 0xFF;
header[30] = (byteRate >> 16) & 0xFF;
header[31] = (byteRate >> 24) & 0xFF;
// 块对齐 = 声道数 * 位深度/8
uint16_t blockAlign = (bitDepth/8);
header[32] = blockAlign & 0xFF;
header[33] = (blockAlign >> 8) & 0xFF;
// 位深度
header[34] = bitDepth & 0xFF;
header[35] = (bitDepth >> 8) & 0xFF;
// data子块标识
header[36] = 'd'; header[37] = 'a'; header[38] = 't'; header[39] = 'a';
// data块大小占位(后续更新)
header[40] = 0; header[41] = 0; header[42] = 0; header[43] = 0;
}
类似的,有时候传感器送出 32Bits 数据,我们需要转为 16Bits输出,编写代码如下:
#include <WiFi.h>
#include <WebServer.h>
#include <driver/i2s.h>
// WiFi配置
const char* ssid = "CMCC-TSR6739";
const char* password = "!!1783az";
// I2S配置
#define I2S_MIC_WS 37
#define I2S_MIC_SD 48
#define I2S_MIC_SCK 45
#define SAMPLE_RATE 16000
#define BITS_PER_SAMPLE 32
#define BUFFER_SIZE 1024
WebServer server(80);
int32_t audioBuffer[BUFFER_SIZE];
int16_t webaudioBuffer[BUFFER_SIZE];
void setup() {
Serial.begin(2000000);
// 初始化I2S
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 8,
.dma_buf_len = BUFFER_SIZE,
.use_apll = false
};
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_MIC_SCK,
.ws_io_num = I2S_MIC_WS,
.data_out_num = I2S_PIN_NO_CHANGE,
.data_in_num = I2S_MIC_SD
};
i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
i2s_set_pin(I2S_NUM_0, &pin_config);
// 连接WiFi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected");
Serial.println("IP address: " + WiFi.localIP().toString());
// 设置HTTP路由
server.on("/stream", HTTP_GET, handleAudioStream);
server.begin();
}
void loop() {
server.handleClient();
// 持续读取音频数据
size_t bytesRead;
i2s_read(I2S_NUM_0, &audioBuffer, BUFFER_SIZE * sizeof(int32_t), &bytesRead, portMAX_DELAY);
}
void handleAudioStream() {
// 生成WAV文件头
uint8_t wavHeader[44];
generateWavHeader(wavHeader, SAMPLE_RATE, 16);
// 发送HTTP头
server.setContentLength(CONTENT_LENGTH_UNKNOWN);
server.send(200, "audio/wav", "");
// 先发送WAV头
server.sendContent((const char*)wavHeader, sizeof(wavHeader));
// 持续发送音频数据
while(server.client().connected()) {
size_t bytesRead;
i2s_read(I2S_NUM_0, &audioBuffer, BUFFER_SIZE*4, &bytesRead, portMAX_DELAY);
for (int i=0;i<BUFFER_SIZE;i++) {
int32_t audio_24bit = audioBuffer[i] >> 8; // 提取高 24 位
webaudioBuffer[i]=(int16_t)((audio_24bit + 128) >> 8); // 四舍五入到 16 位
}
server.sendContent((const char*)webaudioBuffer, bytesRead/2);
}
}
void generateWavHeader(uint8_t* header, uint32_t sampleRate, uint32_t bitDepth) {
// RIFF块标识
header[0] = 'R'; header[1] = 'I'; header[2] = 'F'; header[3] = 'F';
// 文件总大小占位(后续更新)
header[4] = 0; header[5] = 0; header[6] = 0; header[7] = 0;
// WAVE格式标识
header[8] = 'W'; header[9] = 'A'; header[10] = 'V'; header[11] = 'E';
// fmt子块标识
header[12] = 'f'; header[13] = 'm'; header[14] = 't'; header[15] = ' ';
// fmt块大小(16字节)
header[16] = 16; header[17] = 0; header[18] = 0; header[19] = 0;
// 音频格式(1=PCM)
header[20] = 1; header[21] = 0;
// 声道数(1=单声道)
header[22] = 1; header[23] = 0;
// 采样率
header[24] = sampleRate & 0xFF;
header[25] = (sampleRate >> 8) & 0xFF;
header[26] = (sampleRate >> 16) & 0xFF;
header[27] = (sampleRate >> 24) & 0xFF;
// 字节率 = 采样率 * 声道数 * 位深度/8
uint32_t byteRate = sampleRate * (bitDepth/8);
header[28] = byteRate & 0xFF;
header[29] = (byteRate >> 8) & 0xFF;
header[30] = (byteRate >> 16) & 0xFF;
header[31] = (byteRate >> 24) & 0xFF;
// 块对齐 = 声道数 * 位深度/8
uint16_t blockAlign = (bitDepth/8);
header[32] = blockAlign & 0xFF;
header[33] = (blockAlign >> 8) & 0xFF;
// 位深度
header[34] = bitDepth & 0xFF;
header[35] = (bitDepth >> 8) & 0xFF;
// data子块标识
header[36] = 'd'; header[37] = 'a'; header[38] = 't'; header[39] = 'a';
// data块大小占位(后续更新)
header[40] = 0; header[41] = 0; header[42] = 0; header[43] = 0;
}
Easy X用点画阿基米德螺旋线
、

#include <graphics.h> // EasyX图形库头文件
#include <conio.h> // 用于_getch()
#include <math.h>
#define a 0 // 从原点开始
#define b 60*3.1415/180 //半径增加的速率
int main()
{
int x,y;
// 初始化640x480像素的图形窗口
initgraph(640, 480);
for (int i = 0; i < 360*30; i++) {
x = 320 + (a+ b * (i * 3.1415 / 180)) * cos(i * 3.1415 / 180);
y= 240+ (a + b * (i * 3.1415 / 180)) * sin(i * 3.1415 / 180);
putpixel(x, y, YELLOW);
}
// 保持窗口显示
_getch();
// 关闭图形窗口
closegraph();
return 0;
}
运行结果:

一键USB通断器
在一些特别情况下,我们会碰到 USB 兼容性问题。比如,我正在使用的 DELL C2722DE 显示器,通过 TypeC 连接主机,除了能够显示之外,还内置了 USB Hub ,但是它和我的微软鼠标存在兼容性问题,如果插着显示器休眠,或者开机的话,进入 Windows之后鼠标是无法直接使用,必须插拔一次才能正常工作。
针对这种情况,这次设计了一个 USB 插拔装置,遇到问题的时候无需做插拔动作,而是通过按键一次自行完成一个插拔的模拟,这样可以方便使用。USB插拔动作可以分解为2步,插入时是先接通5V供电,然后接通D+/D-信号。如果仔细观察USB接头,可以发现USB D+/D- 引脚会比5V和GND 短一点,这样就能保证插入的时候是先接通供电,然后再接入信号的。这样可以避免插入时信号先于供电接通,电流直接倒灌到芯片中。类似的,拔出时,是先断开信号,然后再切断供电。
硬件方面使用了3个芯片:CH554 /CH442/SY6280AAC。CH554是一个单片机,这里作用是获得按钮状态,然后根据状态控制USB信号的切换和USB母头上的5V输出; CH442进行信号切换, SY6280AAC 是功率电子开关芯片,这里我们用用它控制USB供电输出。
CH442是一款低阻宽带双向模拟开关芯片,包含2路单刀双掷二选一开关。这里用作 USB 信号二选一。


PCB 设计如下:

焊接后的成品,刚好能装在外壳中:

CH554 外围只需要1个10K电阻,2个0.1uf 电容即可让它工作起来。然后通过P1 Header 4 下载数据非常方便。此外,CH_IN 适用于控制CH442信号切换,CTRL1用于控制SY6280AAC。
代码部分非常简单:
#define CH_IN 14
#define LED 15
#define CTRL1 16
#define KEY 17
void setup() {
pinMode(CH_IN,OUTPUT);
pinMode(LED,OUTPUT);
pinMode(CTRL1,OUTPUT);
pinMode(KEY,INPUT_PULLUP);
digitalWrite(CH_IN,LOW); // Switch to 1
digitalWrite(LED,LOW); // 不亮
digitalWrite(CTRL1,HIGH); // 供电
}
unsigned long int Elsp=0;
void loop() {
if (digitalRead(KEY)==LOW) {
digitalWrite(LED,HIGH); // 点亮 LED
digitalWrite(CH_IN,HIGH); // 断开数据线
delay(100);
digitalWrite(CTRL1,LOW); // 断开USB供电
Elsp=millis();
} else {
// Key 抬起
if ((Elsp!=0)&&(millis()-Elsp>500)) {
Elsp=0;
digitalWrite(LED,LOW); // 关闭 LED
digitalWrite(CTRL1,HIGH); // 开始USB供电
delay(100);
digitalWrite(CH_IN,LOW); // 接通数据线
}
}
}
适用范围:如果你遇到的问题通过插拔一次可以解决,并且出现问题的时候USB端口能够正常供电,那么可以尝试本次的设计。运气好的话,因为引入了切换元件使得USB信号发生变化,每次都可以直接用;稍微差一些的话,出现问题的时候按下按钮即可模拟插拔让设备工作起来。
制作一个PCIE设备的空驱动
一些情况下我们需要空驱动来避免设备管理器中出现 Yellow Bang(当然,这种驱动没有任何功能)。这个问题我请教了一下天杀,在他的指导下写了这篇问斩给。
本文介绍如何制作一个 PCIE 的空驱动。
本文提到的软件包括 OpenSSL (来自 https://slproweb.com/products/Win32OpenSSL.html的Win64OpenSSL_Light-3_5_0.exe),此外,签名使用到的文件都来自 Windows SDK 和 WDK。
一.准备用于签名的密钥文件
1. 生成私钥:openssl genrsa -out LABZprivate.key 2048
运行结果:

2. 创建证书签名请求(CSR):
openssl req -new -key LABZprivate.key -out LABZrequest.csr
运行结果:

3.生成自签名证书(.crt)文件
openssl x509 -req -days 365 -in LABZrequest.csr -signkey LABZprivate.key -out LABZcertificate.crt
运行结果:

4.转换为 pfx 格式
openssl pkcs12 -export -out LABZPFX.pfx -inkey LABZprivate.key -in LABZcertificate.crt
会要求你设定一个密码,这里使用 “labz123”
运行结果:

5. 生成 cer 文件
Openssl pkcs12 -in LABZPFX.pfx -clcerts -out LABZcert.cer -nodes
这里输入前面设定的密码 labz123

至此,需要用到的签名的密钥文件已经准备好了。
此外,还可以不用openssl来生成证书,WDK提供了一个更简单的生成自签名证书的工具:makecert.exe。有兴趣的朋友可以继续研究。
二.修改 INF文件
以如下设备为例,查看属性

将上述信息写入EmptyPciDriver.inf

三.使用工具生成 CAT文件以及签名
1.生成 CAT 文件(可以看作是 INF 的签名文件)
“C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x86\inf2cat.exe” /driver:”C:\ndrv ” /os:10_NI_X64

2.生成 INF 的 Hash
“C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\signtool.exe” sign /f LABZPFX.pfx /p labz123 /fd sha256 “c:\ndrv\EmptyPciDriver.cat”

四.测试
1.将前面的公钥加入目标机中(只用于测试,不要用于工作机)
Certutil -addstore root LABZcert.cer
2.将公钥加入受信任的发布者列表中
Certutil -addstore TrustedPublisher LABZcert.cer
然后像普通驱动一样安装即可:

安装后设备管理器中可以看到:

测试完成之后,拿去做正式签名,之后对于相同 VID DID SVID SDID的PCIE设备就可以直接使用了(无需步骤四提到在目标机中导入公钥的步骤)
本文提到的 INF 文件下载:
本文生成的签名文件:
ImageMagick VC 代码的一些细节
大多数时候命令行足够用了,但是对于一些无法写入同一条命令行的组合操作,如果直接使用 VC 编程可以大大提升效率。
// 读取文件
image.read(filename);
// 改变大小
image.resize(Geometry(image.columns() * resize/100, image.rows() *resize / 100));
// 设置背景颜色用于填充
image.backgroundColor(Color("Yellow"));
//旋转
image.rotate(angle);
// 特别主意,如果没有下面这个 repage 会导致计算坐标有问题
image.repage();
//以中心为原点裁剪图片
image.crop(Geometry(1920, 1080, (image.columns()/2 - 1920/2), (image.rows()/2 - 1080/2)));
// 保存图片
image.write(Output);
参考: