Step to UEFI (278)Progra message 的使用

Visual Studio 的 C 支持 #pragma message() 宏可以用来输出一些信息。于是编写一个代码进行测试:

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

/***
  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
  )
{
  #pragma message (__FILE__)
    return(0);
}

唯一的问题是:我在 EDK2 中编译的时候,无法看到输出的结果。经过研究,编译C代码是通过下面这个指令:

"C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Tools\MSVC\14.29.30133\bin\Hostx86\x64\cl.exe" /Foc:\buildbs\edk2-edk2-stable202205\Build\AppPkg\DEBUG_VS2019\X64\AppPkg\Applications\HelloMacro\HelloMacro\OUTPUT\.\ /showIncludes /nologo /c /WX /GS- /W4 /Gs32768 /D UNICODE /O1b2s /GL /Gy /FIAutoGen.h /EHs-c- /GR- /GF /Z7 /Gw /X /Zc:wchar_t /D UEFI_C_SOURCE /Wv:11 /Ic:\buildbs\edk2-edk2-stable202205\AppPkg\Applications\HelloMacro  /Ic:\buildbs\edk2-edk2-stable202205\Build\AppPkg\DEBUG_VS2019\X64\AppPkg\Applications\HelloMacro\HelloMacro\DEBUG  /Ic:\buildbs\edk2-edk2-stable202205\MdePkg  /Ic:\buildbs\edk2-edk2-stable202205\MdePkg\Include  /Ic:\buildbs\edk2-edk2-stable202205\MdePkg\Test\UnitTest\Include  /Ic:\buildbs\edk2-edk2-stable202205\MdePkg\Include\X64  /Ic:\buildbs\edk2-edk2-stable202205\ShellPkg  /Ic:\buildbs\edk2-edk2-stable202205\ShellPkg\Include @c:\buildbs\edk2-edk2-stable202205\Build\AppPkg\DEBUG_VS2019\X64\AppPkg\Applications\HelloMacro\HelloMacro\OUTPUT\cc_resp_1.txt

于是尝试去掉其中的/nologo 指令,运行结果如下:

再同时去掉 /showIncludes 运行结果:

可以看到,其中出现了当前的文件名信息。

总结:在使用VC 编写代码的时候,如果需要输出一些编译期的数据,可以考虑使用 #pragma message() 来实现。

海龟汤和“死尸粒子”

在国外流行着一种叫做海龟汤的游戏。正规的名字是情境猜谜(Situation puzzle),又译情境推理游戏,俗称“海龟汤”,另名水平思考游戏(Lateral thinking puzzle)或是/不是游戏,是一种猜测情境型事件真相的智力游戏。其玩法是由出题者提出一个难以理解的事件,参与猜题者可以提出任何问题以试图缩小范围并找出事件背后真正的原因,但出题者仅能则以“是(对)”、“不是(不对)”或“没有关系”来回答问题。

一个典型的游戏是,出题者提问:“一个男人走进一家酒吧,并向酒保要了一杯水。酒保拿出一支枪并瞄准他,该名男子说:“谢谢你!”然后离开,请问发生了什么事?”

猜题者与出题者的问、答过程可能如下:

问:酒保听得到他说的话吗? 答:是

问:酒保是为某些事情生气吗? 答:不是

问:这支枪是水枪吗? 答:不是

问:他们原本就互相认识吗? 答:毫无关系

问:这个男人说“谢谢你”时带有讽刺的口气吗? 答:没有

问:酒保认为男子对自己构成威胁吗? 答:没有

经过一番问答之后,可能会导引出答案:该名男子打嗝,他希望喝一杯水来改善状况。酒保意识到这一点,选择拿枪吓他,男子一紧张之下,打嗝自然消失,因而衷心感谢酒保后就离开了。[参考1]

然而,这种游戏在国内几经传播之后就只剩下一个匪夷所思的题目,然后要求参与者直接告推断出过程,或者说需要参与者临时编造一个听起来合理的故事。最典型的就是“有母女三人,母亲死了,姐妹两去参加葬礼,妹妹在葬礼上遇见一个很有型的男子,并对他一见倾心,回到家后妹妹把姐姐杀了,为什么?”然后这种题目还被冠以“FBI犯罪心理测试”这样的标题。

显而易见整个玩法和之前的大相径庭,甚至有了更多莫名其妙的意味。

接下来介绍一下100多年前,一个关于“死尸粒子”的事情。

在工业化国家中,目前婴儿出生死亡率为十万分之九:而在180年前,婴儿出生死亡率是现在的5o多倍。当时分娩所面临的最严重的威胁,是一种往往导致产妇和婴儿双双死亡的致命疾病,叫产褥热。

19世纪40年代,欧洲最好的医院,例如伦敦产科总医院、巴黎产科医院、德累斯顿产科医院,都饱受这种病症的威胁。

临产的孕妇到达医院时还是健康的,生产过后不久,就会莫名其妙地染上产褥热,最后死去。或许,维也纳总医院算得上是当时最好的医院。1841–1846年,医生接生的婴儿超过20000个,大约有2000名产妇死亡,死亡率为1/10。

1847年,情形进一步恶化: 死于产褥热的产妇比例已达1/6。就在那一年年,匈牙利籍医生塞梅尔维斯成为维也纳总医院院长助理。塞梅尔维斯敏感细腻,对病人体贴入微,对他们所遭受的痛苦总能感同身受。看到产妇生产过后纷纷死去,他陷入深深的苦恼之中,于是便着了魔似的要改变这种情形。与其他很多过于情绪化的人不同,塞梅尔维斯能够做到把感情搁置一边,集中心思分析事实,不论是已知的还是未知的。他聪明地得出的第一个结论是,事实上医生对产褥热发生的原因一无所知。那些医生或许会说他们知道,但异常高的死亡率表明他们并不知情。我们来回顾一下过去就会发现,

当时医生认为导致产褥热病的诸多“原因”,事实上都是彻头彻尾的瞎猜: 

  • 妊娠早期行为不当,比如穿紧身胸衣和衬裙太紧;子宫内的胎儿使排泄物流通不畅,滞留于肠内,而其中已分解腐烂的液体则融入血液之中。
  • 形成乳汁的过程中产生的臭气所致;恶露郁阻;宇宙一-地球磁力影响;个人体质欠佳……
  • 产房的空气恶臭。
  • 男医生接生,或许这玷污了产妇的贞洁,进而导致了病变。
  • 受凉;饮食不当;生产之后,急于回归正常作息,从分娩室出来得太早。

耐人寻味的是,产妇死亡的原因总被归因于她们自己。这可能与一个事实有关:当时所有的医生都是男性。如今看来,19世纪的医学似乎仍然很原始,但那时的医牛地位啡凡,俨然是智慧和权威的化身。

然而,产褥热的肆虐却让他们一筹莫展,地位受到严峻挑战:如果是在家由产婆接生(这在当时仍很普遍),那么产妇死于产褥热的概率比在医院生产后死亡的概率小得多,不过是后者的1/6o。

当时医生都受过最好的医学训练;而如果在家里生产产妇往往躺在凹凸不平的床垫上,由农村的产婆接生。那么,产妇在医院分娩的危险性为什么会更高呢?

为了破解这个谜题,塞梅尔维斯开始认真分析数据。在自己效力的医院收集产妇死亡率的数据后,他发现了一个非常奇怪的现象。这家医院有两种产房,其中一种产房由男医生和习生负责,另一种则由女接生员和实习生负责。而这两种产房中的产妇死亡率非常悬殊:

男医生负责的产房的死亡率是女接生员负责的产房的两倍多,这到底是为什么呢?

塞梅尔维斯想弄清楚的是,在男医生负责产房中分娩的孕妇,是否本身有严重的病情,体质更差,或是有其他方面的潜在病因。

不是,不可能是这样。临产孕妇被分配到哪种产房,这取决于她们是在一周中哪一天到达医院的,因为这两种产房以24小时为间隔轮流接纳临产孕妇。鉴于妊娠期是可以计算的,因此孕妇会在产期来临时去医院,而不是在其他方便的日子。这种分配方法虽然算不上是严格的随机,然而就塞梅尔维斯所要探究的问题而言,这的确暗示了一个事实:两种产房死亡率的差别,并不是由两种产房接纳临产孕妇总人数上的差异所导致的。

也许,上面所列出的一种胡乱猜测是事实:在为产妇接生的这种敏感而微妙的任务中,从某种程度上说,正是男性的在场害死了那些产妇?

塞梅尔维斯认定,这也是不太可能的。对两种产房中出生的婴儿死亡率进行分析后,他还发现了这样的事实: 男医生负责的产房的婴儿死亡率比女接生员负责的产房高很多,分别为9.9%和3.9%。男婴和女婴的死亡率并没有什么不同。正如塞梅尔维斯所观察到的,新生婴儿“因为男医生接生而死亡”是不太可能的。因此,认为男性在场是那些产妇死亡的原因的推断是站不住脚的。

当时还有一种推测是这样的:男医生负责的产房接纳的临产孕妇,此前听说这里的死亡率很高,所以“惊恐万分,结果导致她们也染上了这种疾病”。塞梅尔维斯也不认同这种解释:”我们可以设想一下,在杀人无数的血腥战争中,士兵也一定惧怕死亡。然而,这些士兵并没有染上产褥热。”

不可能。男医生负责的产房必定有其特殊的地方,那可能是导致产褥热病的原因。

到目前为止,塞梅尔维斯已经确认了几个事实: 

  • 即便在大街上分娩,随后才去医院的那些最贫穷的产妇,也没有患产褥热。
  • 子宫颈扩张超过24小时的产妇,“几乎毫无例外地都染上了产褥热。
  • 医生没有因接触产妇或新生婴儿而染上疾病,因此,几乎可以肯定的是这种病不具有传染性。

然而,他仍然困惑不已。“一切因素都得考虑,一切都难以解释,一切都令人生疑。”他这样写道,”唯有一个事实不容置疑,那就是为数众多的死亡人数。”

一个悲剧发生后,他终于找到了答案。塞梅尔维斯所推崇的一位老教授,在一次不幸的医学事故发生后很快就去世了。当时,老教授带着一个学生做尸体解剖实验突然那个学生的手术刀滑了-下,伤着了老教授的手指。塞梅尔维斯注意到,老教授死前的诸多症状,例如胸膜炎、心包炎、腹膜炎及脑膜炎,“与数百例患产褥热的产妇死前的症状相似”。

教授的死因不是什么难解之谜。他死于“已进入他血管系统的死尸粒子”(cadaverous particle),塞梅尔维斯这样写道。那些死去的产妇,是否也有这种死尸粒子进入了血管系统呢?

当然!

那个时期,维也纳总医院和其他一流的医学院,都日益专注于研究解剖学,基本教学手段就是尸体解剖。对需要了解疾病大致情况的医学院学生而言,有什么比双手拿起衰竭的器官密切观察,进而在血液、尿液或胆汁中找出蛛丝马迹更好的方法吗?在维也纳总医院,每一个死去的病人,包括死于产褥热的产妇,都被直接送往解剖室。

离开解剖室后,医生和学生往往直接去了产房,至多洗一下手而已。要知道,直到此后10年或20年,医学界才接受细菌理论。后来的细菌理论证实,很多疾病是活着的微生物引起的,而不是动物神灵、陈腐的空气,也不是腹带太紧所致。在当时,塞梅尔维斯弄明白了这其中的缘由。引发产妇产褥热的罪魁祸首正是医生,因为是他们将死尸粒子带给了产妇。

这解释了男医生负责的产房的死亡率比女接生员负责的

产房的死亡率高得多的事实。同样,男医生负责的产房的死亡率为什么比在家中甚至在大街上分娩更高?为什么子官颈扩张时间越长,产妇就越容易患上产褥热?这一切都有了合理的解释。子宫颈扩张时间越长,这个产妇就越是需要医生和学生助产,而伸进(可能伤及)子宫的那只手,因为刚做过解剖实验,还留存有死尸粒子。

“我们中没有一个人知道,”塞梅尔维斯后来懊悔地说,“正是我们自己导致了无数人的死亡。”

得益于他的发现,这场瘟疫终于得到控制。他命令所有医生和学生,做完尸体解剖手术后双手都必须用含氯消毒水消毒。男医生负责的产房的死亡率大幅下降,降至1%。在此后的12个月中,塞梅尔维斯实施的措施,挽救了300位母亲和250个婴儿的生命,这仅仅是一家医院的一个产房所挽救的生命总数。【参考2】

如果将上面塞梅尔维斯的例子用海龟汤的方式描述,问题就是“为什么180年前,欧洲的各大医院产妇和婴儿更容易因为‘产褥热’死亡?” 如果能够刨除各种事实,前面关于这个问题的各种猜想都非常“合情合理”。从这里可以看出:单纯的逻辑推理、思想实验是无法得知事情的真相的,甚至会很容易得到错误的结论,只有统计和实验能够确定真正的原因。

更悲哀的是,即便找到了最终的答案,例如:医生护士没有执行消毒导致产妇和婴儿的微生物感染。仍然会有“哲学家”跳出来说这个答案我早就知道,比如:中医说,很明显这个是外邪入侵导致的,我们早知道,下次信我;或者信佛的人说,所谓“佛观一钵水,八万四千虫”,我这是大智慧。

参考:

  1. https://baike.baidu.com/item/%E6%83%85%E5%A2%83%E7%8C%9C%E8%B0%9C/2419095?fr=ge_ala
  2. 《魔鬼经济学2》

Step to UEFI (277)QEMU 增加自定义的 FFS和读取

这次实验的是在 OVMF 生成的BIOS中插入一个Binary ,然后在代码中将这个Binary 读取出来。

第一个目标:在 OVMF 中插入 Binary。

1.我们准备一个 message.txt,其中内容是简单的字符串:

This is a test message comes from 
www.lab-z.com

2.在\OvmfPkg\OvmfPkgX64.fdf 文件中,加入下面的代码

!if $(E1000_ENABLE)
  FILE DRIVER = 5D695E11-9B3F-4b83-B25F-4A8D5D69BE07 {
    SECTION PE32 = Intel3.5/EFIX64/E3522X2.EFI
  }
!endif

#LABZDebug_Start
FILE FREEFORM = C3E36D09-2023-0829-A857-D5288FE33E28 Align=4K {
  SECTION RAW = OvmfPkg/LabzBin/message.txt
}
#LABZDebug_End

!include NetworkPkg/Network.fdf.inc
  INF  OvmfPkg/VirtioNetDxe/VirtioNet.inf

3.使用工具查看放置的FFS,可以看到正确的增加到 BIOS 中

这样,第一个目标已经完成,我们成功的生成了一个FFS文件。

第二个目标,将这个 FFS文件从FV中读取出来。之前我们做过类似的实验,在【参考1】中有介绍。这次我们编写一个 UEFI Shell Application ,显示前面插入的 FFS文件的内容。测试代码如下:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include  "PiFirmwareVolume.h"
#include  "PiFirmwareFile.h"
#include  "FirmwareVolume2.h"

INTN
EFIAPI
ShellAppMain (
    IN UINTN Argc,
    IN CHAR16 **Argv
)
{
	CONST EFI_GUID    NameGuid= { 0xC3E36D09, 0x2023, 0x0829,
		{ 0xA8, 0x57, 0xD5, 0x28, 0x8F, 0xE3, 0x3E, 0x28 }
	};
	EFI_SECTION_TYPE  SectionType=EFI_SECTION_RAW;

	VOID             *Buffer=NULL;
	UINTN             Size=0;
	UINT32            AuthenticationStatus=0;
	EFI_STATUS  	  Status;
	EFI_FIRMWARE_VOLUME2_PROTOCOL *Fv;

	Status = gBS->LocateProtocol (
	             &gEfiFirmwareVolume2ProtocolGuid,
	             NULL,
	             (VOID **) &Fv
	         );
	if (EFI_ERROR (Status))
	{
		Print(L"[EFI_FIRMWARE_VOLUME2_PROTOCOL not found]\n");
		return EFI_NOT_FOUND;
	}
	//
	// Read desired section content in NameGuid file
	//
	Status      = Fv->ReadSection (
	                  Fv,
	                  &NameGuid,
	                  SectionType,
	                  0,
	                  &Buffer,
	                  &Size,
	                  &AuthenticationStatus);
	UINT8 *P=(UINT8 *)Buffer;
	Print(L"[EFI_FIRMWARE_VOLUME2_PROTOCOL %r]\n",Status);

	for (UINTN i=0; i<Size; i++)
	{
		Print(L"%c",P[i]);
	}
	Print(L"\n");

	return(0);
}

运行的结果如下图所示,可以看到正确的读取出我们存放的内容:

完整的代码下载:

参考:

1. https://www.lab-z.com/getffs/  代码读取一个 FFS

Step to UEFI (276)宏和结构体初始化表格

在 EDK2 中有一种比较有趣的定义和初始化Table 的方法,主要是基于 __VA_ARGS__ 这个宏。

“__VA_ARGS__是一个预处理宏,用于表示可变数量的参数。当在宏定义中使用__VA_ARGS__,它会自动展开为传递给宏的实际参数。以下是一个示例使用__VA_ARGS__的宏定义代码:
#include &lt;stdio.h>
 
#define PRINT_ARGS(...) printf(__VA_ARGS__)
 
int main() {
    PRINT_ARGS("Hello, %s!\n", "World");
    return 0;
}
上述代码中,宏定义PRINT_ARGS使用__VA_ARGS__来表示可变数量的参数,并通过printf函数打印参数。在main函数中,我们调用PRINT_ARGS宏来打印字符串"Hello, World!"。运行结果为输出"Hello, World!"。
总结:__VA_ARGS__是一个用于表示可变数量参数的预处理宏,在宏定义中使用它可以方便地处理不定数量的参数。“----来自百度

很多时候,我们定义一个 Table 用来传递一些常量,Table需要给出具体的长度,通过这个宏可以实现自动给出Table 的长度,避免用户手工计数的麻烦。

下面是一个示例代码:

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

#define MY_TABLE_INIT(Vid,Did,...) \
{ \
  { Vid, Did, (sizeof((UINT32[]){__VA_ARGS__})/sizeof(UINT32)) }, \
  { __VA_ARGS__ } \
}

typedef struct {
  UINT16  VendorId;
  UINT16  DeviceId;
  UINT16  DataDwords;
} MY_TABLE_HEADER;

typedef struct  {
  MY_TABLE_HEADER  Header;
  UINT32 Data[];
} ONE_TABLE;

ONE_TABLE Table = MY_TABLE_INIT (
  0x1234, 0x5678,
  
  // Raw Data
  0x01234567,
  0x89ABCDEF,
  0xFEDCBA98,
  0x76543210
);

INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  )
{
  Print(L"Table:\n");
  Print(L"   VendorId:[%X]\n",Table.Header.VendorId);
  Print(L"   DeviceId:[%X]\n",Table.Header.DeviceId);
  Print(L"   Size    :[%X]\n",Table.Header.DataDwords);  
  
  for (int i=0;i<Table.Header.DataDwords;i++) {
	  Print(L"[%04X]",Table.Data[i]);  
  }
  Print(L"\n");
  return(0);
}

运行结果如下:

上面代码的解释如下:

1.首先我们定义一个 ONE_TABLE 结构体用来“携带”数据。

typedef struct  {
  MY_TABLE_HEADER  Header;
  UINT32 Data[];
} ONE_TABLE;

从定义可以看到,这个结构体包含了一个头,还有一个变长的数据段。头可以实现用于识别判断这个Table 是否为我们需要的目的,例如,其中有DID和VID 信息。具体定义如下,特别注意 DataDwords 给出了后面变长数据段的长度:

typedef struct {
  UINT16  VendorId;
  UINT16  DeviceId;
  UINT16  DataDwords;
} MY_TABLE_HEADER;

对于DataDwords 就是我们前面提到的“Table需要给出具体的长度”的问题。

2.为了解决上述问题,通过下面的宏来解决:

#define MY_TABLE_INIT(Vid,Did,...) \
{ \
  { Vid, Did, (sizeof((UINT32[]){__VA_ARGS__})/sizeof(UINT32)) }, \
  { __VA_ARGS__ } \
}

其中(sizeof((UINT32[]){__VA_ARGS__})/sizeof(UINT32)) 就是计算长度的代码。最终的结果是以 UINT32(DWORD)给出的。

3.初始化定义如下,可以看到DataDwords的计算是宏直接完成的,并不需要我们直接提供

ONE_TABLE Table = MY_TABLE_INIT (
  0x1234, 0x5678,
  
  // Raw Data
  0x01234567,
  0x89ABCDEF,
  0xFEDCBA98,
  0x76543210
);

可以看到,通过上面的方法可以帮助我们方便的实现可变数据的长度定义,有兴趣的朋友不妨尝试一下。

完整的代码下载:

批处理延时和计算经过时间

首先介绍一下批处理中延时的实现:下面代码实现延时3秒

CHOICE /T 3 /C ync /CS /D y

计算经过时间,以秒为单位:

@echo off
set "t=%time%"
::You code start here

::You code end here
set "t1=%time%"

if "%t1:~,2%" lss "%t:~,2%" set "add=+24"
set /a "times=(%t1:~,2%-%t:~,2%%add%)*3600+(1%t1:~3,2%%%100-1%t:~3,2%%%100)*60+(1%t1:~6,2%%%100-1%t:~6,2%%%100)" 
echo Time Used %times% Seconds
pause

上述代码合在一起进行测试:

@echo off
set "t=%time%"
::You code start here
CHOICE /T 3 /C ync /CS /D y
::You code end here
set "t1=%time%"

if "%t1:~,2%" lss "%t:~,2%" set "add=+24"
set /a "times=(%t1:~,2%-%t:~,2%%add%)*3600+(1%t1:~3,2%%%100-1%t:~3,2%%%100)*60+(1%t1:~6,2%%%100-1%t:~6,2%%%100)" 
echo Time Used %times% Seconds
pause

将一个文件中的多个 Sheet 内容合并的VBA

如下的 VBA 代码可以帮助我们将一个Excel文件中的多个 Sheet 合并到一起:

Sub Merge_Sheets()
    'Insert a new worksheet
    Sheets.Add
     
    'Rename the new worksheet
    ActiveSheet.Name = "ProfEx_Merged_Sheet"
     
    'Loop through worksheets and copy the to your new worksheet
    For Each ws In Worksheets
        ws.Activate
         
        'Don't copy the merged sheet again
        If ws.Name &lt;> "ProfEx_Merged_Sheet" Then
            ws.UsedRange.Select
            Selection.Copy
            Sheets("ProfEx_Merged_Sheet").Activate
             
            'Select the last filled cell
            ActiveSheet.Range("A1048576").Select
            Selection.End(xlUp).Select
             
            'For the first worksheet you don't need to go down one cell
            If ActiveCell.Address &lt;> "$A$1" Then
                ActiveCell.Offset(1, 0).Select
            End If
             
            'Instead of just paste, you can also paste as link, as values etc.
            ActiveSheet.Paste
         
        End If
         
    Next
End Sub

来源:

Merge Sheets: Easily Copy Excel Sheets Underneath on One Sheet!

ESP32 S2 Mini

最近发现了一款非常便宜的 ESP32 S2 开发板:wemos 的 ESP32 S2 MINI,价格在12元。这个建议甚至低于 Atmel 328P 芯片,更重要的是这个是开发板直接可以下载代码无需额外 USB转串口设备。

官方网站是 https://www.wemos.cc/en/latest/s2/s2_mini.html#

  • based ESP32-S2FN4R2 WIFI IC
  • Type-C USB
  • 4MB Flash
  • 2MB PSRAM
  • 27x IO
  • ADC, DAC, I2C, SPI, UART, USB OTG
  • Compatible with LOLIN D1 mini shields
  • Compatible with MicroPython, Arduino, CircuitPython and ESP-IDF
  • Default firmware: MicroPython

对于一般的开发已经足够用了。

在使用 Arduino 开发时,需要特别注意选择为  LOLIN S2 MINI 开发板,具体如下:

引脚定义在下面这个文件中(ESP32的大多数引脚都可以自行定义,但是为了更好的兼容,个人建议使用预定义值)

C:\Users\UserName\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.6\variants\lolin_s2_mini\pins_arduino.h

Arduino ESP32 I2C Slave 的例子

Arduino 作为 I2C Slave 算是比较冷门的使用方式,下面是一个实际的例子:

#include <Wire.h>
 
byte i2c_rcv=0;               // data received from I2C bus
 
void setup() {
  Wire.begin(0x08);           // join I2C bus as Slave with address 0x08
 
  // event handler initializations
  Wire.onReceive(dataRcv);    // register an event handler for received data
  Wire.onRequest(dataRqst);   // register an event handler for data request
  Serial.begin(115200);
}
 
void loop() {
}
 
//received data handler function
void dataRcv(int numBytes) {
  Serial.print("Slave Received ");
  Serial.print(numBytes);
  Serial.println("Bytes");
  while (Wire.available()) { // read all bytes received
    i2c_rcv = Wire.read();
    Serial.print("[");
    Serial.print(i2c_rcv);
    Serial.print("]");
  }
  Serial.println("");
}
 
// requests data handler function
void dataRqst() {
    Wire.write(i2c_rcv); // send potentiometer position
    Serial.print("Slave send ");
    Serial.print(i2c_rcv,HEX);
}

运行之后,Arduino 作为一个地址为 0x08 的I2C设备。当它收到 Master 发送过来的数据,会进入 void dataRcv(int numBytes)  函数,然后将收到的数据输出到串口上;当它收到 Master 发送的读请求,会进入void dataRqst() 函数,将之前收到的数据返回给 Master 。

试验使用 Leonardo 板子,使用调试器发送 10 17 表示对 0x08 地址的设备发送 0x17,之后调试器发送 11 01 表示从 0x08 设备读取一字节数据。

ESP32 I2C Slave Mode

ESP32 目前支持 I2C 的 Slave Mode ,就是说可以作为一个 I2C 设备存在。

具体的介绍在下面能看到,是一个大佬写了一个 Slave Mode 的库,后来整合到了官方 Release 中。

https://github.com/espressif/arduino-esp32/pull/5746/commits/f9f70d2f73d16f7fb50f59e05323cd041acce830

安装完最新的ESP32 Arduino支持包之后,可以在下面的路径中看到:

C:\Users\UserName\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.6\libraries\Wire

其中的 WireSlave 就是实现一个 I2C Slave 的完整例子。