废话不谈,通过三个实际的开发项目,分享一下VFR开发过程中的心得和应该注意的问题:
1.Set Data And Time
龙芯4000上的实现的原理:
之前4000上是在BdsDxe中实现的,熟悉Loongson平台的4000的话,我们不难看的出来:
设置时间和日期的功能是在BdsDxe/BootMaint/BootMaint.c中调用BootMaintCallback实现的。
在BootMaintCallback,会通过检测相关的QuestionId来进行不同的动作,比如说:
case FORM_BOOT_CHG_ID:
case FORM_DRV_CHG_ID:
UpdatePageBody (QuestionId, Private);
break;
再比如说这次需要加的功能:设置时间和日期的QuestionId,它对应的QuestionId是:
case FORM_DATESET_ID:
UpdateDatePage (Private);
break;
这里面有两个关键的问题:
1.回调函数的QuestionId是谁传过来的?
在VFR文件中定义的!VFR文件是定义我们在Setup界面上的所有的显示项(元素)的。
比如FORM_DATESET_ID这个QuestiomId与Bm.vfr文件中的Key对应起来:
通过goto语句可以定义一个新的form,然后在这个form里面,可以注册这个Key:
label FORM_DATESET_ID;
label LABEL_END;
2.4000上如何完成时间的显示?
上面所描述的FORM_DATESET_ID这个Key就是上面所说的传给CallBack函数的。
然后在UpdatePageBody (QuestionId, Private);函数里面调用:
HiiCreateTimeOpCode (
mStartOpCodeHandle,
0x8005,
0x0,
0x0,
STRING_TOKEN(STR_TIME_SAMPLE_TITLE),
STRING_TOKEN(STR_TIME_SAMPLE_HELP),
0,
QF_TIME_STORAGE_TIME,
NULL
);
以此来完成时间的显示.
5000平台上,我舍弃了4000上的做法:
在5000平台上,我没有使用4000上BdsDxe驱动,而是Override了公共代码里面的
BootMaintenanceManagerUiLib,DeviceManagerUiLib,BootManagerUiLib
这三个Lib,在这里面没有再对时间进行显示的接口,只有一些通用的,
比如说,有Boot Options,Driver Options 等等。
当然:我可以在BootMaintenanceManagerUiLib
里面增加一个BootMaintCallback,并在BootMaintCallback函数里面,调用:
BootMaintCallback,(HiiCreateTimeOpCode )这种形式完成对时间的添加,
但是这种方式不好,原因是:
a.Override的BootMaintenanceManagerUiLib没有这个功能,添加之后,违背了
BootMaintenanceManagerUiLib原本想要提供的功能,代码的复用性不好.
b.时间的显示应该和BootMaintenanceManager,DeviceManager,BootManager
属于同一级的目录。也应该在FrontPage下面。
所以,基于以上的原因,最好是自己写一个驱动,或者在SampleCode里面增加时间的显示。
自己写驱动去注册的时候,有两种办法可以注册到Setup界面下面:
1.采用和4000平台一样的办法,在VFR文件中注册一个label和label,使用HiiCreateTimeOpCode
这种方式,将时间动态的添加到label和label下面。
2.EDKII提供了另一种方法:
使用time标签,新语法如下:
time
prompt = STRING_TOKEN(STR_TIME_PROMPT),
help = STRING_TOKEN(STR_TIME_HELP),
flags = STORAGE_TIME,
endtime;
注意:VfrCompile最终会把STORAGE_TIME,编译成QF_TIME_STORAGE_TIME。
关于time,上面描述的是新的语法,新的语法增加了flags,flags包含三种形式:
STORAGE_TIME,STORAGE_NORMAL,STORAGE_WEAKUP,
注意:这三个宏不能同时使用。
STORAGE_TIM就是正常的显示时间,
STORAGE_NORMAL显示的时间是不会增长的,
STORAGE_WEAKUP是设置唤醒时间。
当然了,新语法中也可以写default ,顾名思义就是设置默认的时间.
使用time标签,老的语法:
time hour varid = Time.Hour,
prompt = STRING_TOKEN(STR_TIME_PROMPT),
help = STRING_TOKEN(STR_TIME_HELP),
minimum = 0,
maximum = 23,
step = 1,
default = 0,
minute varid = Time.Minute,
prompt = STRING_TOKEN(STR_TIME_PROMPT),
help = STRING_TOKEN(STR_TIME_HELP),
minimum = 0,
maximum = 59,
step = 1,
default = 0,
second varid = Time.Second,
prompt = STRING_TOKEN(STR_TIME_PROMPT),
help = STRING_TOKEN(STR_TIME_HELP),
minimum = 0,
maximum = 59,
step = 1,
default = 0,
endtime;
在老的语法里面没有flags,写起来不是特别的方便,所以建议直接使用新的语法。
我们看,不管是使用time标签,还是在VFR文件中注册一个label和label的方式,
最终都是要调用到QF_TIME_STORAGE_TIME,QF_TIME_STORAGE_TIME这个宏是在
SetupBrowserDxe/Setup.c中初始化的,在Setup.c里面,Browser会调用
GetQuestionValue(...)这个函数完成对QuestionId值的获取,获取到QF_TIME_STORAGE_TIME
之后会调用gRT->SetTime (&EfiTime);和 gRT->GetTime (&EfiTime, NULL);
这两个函数获取并设置RTC寄存器的值,获取相应的时间信息。
添加时,采取的是上述第二种方法进行时间的显示的。第二种方法的优势在于,
写驱动程序的时候,可以不关注CallBack函数,不必在CallBack函数中调用HiiCreateTimeOpCode
并动态的注册到Setup界面.使得程序更加简洁,优雅。
可能存在的问题:
在添加完时间显示的标签之后,发现在5000的板子上不能够正确的显示并设置时间,
而且从RTC寄存器中读出来的值是脏数据,加打印信息进行调式,发现
gRT->GetTime(&EfiTime, NULL);读回来的值是不正确的,在同一批机器的其他板子上进行实验,
发现没有这样的问题,最后是使用的板子本身的RTC硬件存在问题.
2.Password
龙芯4000上的实现方式:
4000上的实现是通过在BdsDxe驱动下的BootMaint.c中调用UpdatePasswordPage和
UpdateClearPasswordPage两个函数分别实现密码的设置和清除功能的。
他们都是依附在 Boot Maint Manager下的。
UpdatePasswordPage函数中使用以下完成对密码的设定和显示:
HiiCreatePasswordOpCode (
mStartOpCodeHandle,
(EFI_QUESTION_ID)BOOT_TIME_OUT_QUESTION_ID,
VARSTORE_ID_BOOT_MAINT,
BOOT_TIME_OUT_VAR_OFFSET,
STRING_TOKEN (STR_PASSWORD),
STRING_TOKEN (STR_HLP_PASSWORD), 、
EFI_IFR_FLAG_CALLBACK,
0,
6,
20,
NULL
);
这样的话,我们就需要在BootMaintCallback中去调用它并显示在Setup界面中。
UpdateClearPasswordPage函数完成对密码的清除功能:
gRT->GetVariable(
L"Passwd",
&mPasswdSetupGuid,
NULL,
&Size_old,
&mPassword
);
获取”Passwd”所存的旧密码,通过CreatePopUp的方式将用户输入的密码存到Loongson
数组中,对比数组中输入的密码和之前在UpdatePasswordPage函数中所存的密码是否相同,
如果相同,就将“Passwd”这个Variable设置为空。完成对密码的清除功能。
总结:虽然4000平台上通过以上的方式实现了密码的设置和清除功能.
但是存在四个致命的问题:
1.密码的存储是以明文的形式存储的,可以dump 出来
2.密码耦合程度高
3.必须依附在BdsDxe的某一个界面下,并通过在Callback函数中调用才能完成相应的功能。
4 通过Variable的方式实现起来过于复杂
5000上最终的实现方式:
通过调研,发现了UEFI下一个标准的实现Password功能的方式,哈哈哈哈,
可以说是功夫不负有心人了!
在哪儿呢?
路径:EdkCompatibilityPkg/Sample/Tools/Source/VfrCompile/VfrCompile.g
墙裂建议:所有做Setup界面的工作的,都应该好好的看一看这个文件。
他是制定Vfr文件编译格式和语法规则,以及解析的格式的!而且相当的详尽!
之前在其他的项目中,总是有这样一种感觉:VFR文件一发生改变,编译器就总是报类似于:
: unexpected token VfrCompile: ERROR 0003: Error parsing这样的错误信息,
那是因为我们没有按照VfrCompile.g中所规定的格式写。
在VfrCompile.g文件中规定了OneOf,Numeric,Date,Time,Password,String,
SuppressIf,Hidden, Goto,GrayOutIf,InconsistentIf,Label ,Banner,OrderedList,
SaveRestoreDefaults等标签的使用方法和语法规则。
Password 的使用格式为:
password varid = MyNvData.Password,
prompt = STRING_TOKEN(STR_PASSWORD_PROMPT),
help = STRING_TOKEN(STR_PASSWORD_HELP),
Minsize = 6,
maxsize = 20,
encoding = 1,
endpassword;
在Vfr文件中,使用password标签就可以实现密码的设置和清除功能,
不需要再在c文件中写多余的代码
当然了,我是实现了Hash存储的,这部分代码设计到国家信息技术产业的机密,不便展示.请理解.
恢复出厂设置功能:
该功能和4000上的实现思路一样,使用SpiFlashService擦除相关的Variable区域
即可实现恢复出厂那个设置的功能。有兴趣的可以阅读Password.c中的代码。
3.WeakupOnAlarm
顾名思义,WeakupOnALarm 指的就是定时唤醒,
在UEFI Setup界面下添加相关的功能可以实现系统的定时唤醒功能。
User Define means: 用户输入唤醒了次数和延时的时间(Weakup Times, Delay Time).
Minute Event: 从当前时间开始,每隔1分钟之后,唤醒一下系统。
Hour Event : 从当前时间开始,每隔1小时之后,唤醒一下系统。
Daily Event : 从当前时间开始,每隔1小时之后,唤醒一下系统。
Weekly Event: 从当前时间开始,每隔1周之后,唤醒一下系统。
Single Event : 由用户输入想要唤醒的时间,到达所设定时间之后,唤醒系统。
如何实现呢?
实现思路:
User Define,Minute Event,Hour Event,Daily Event,Weekly Evnet的实现方式一致,
都是获取到系统的当前时间之后,循环的增加固定的时间,
并写入 SYS_TOYMATCH0寄存器中,定时的唤醒系统,这里不再赘述。
SingleEvent的实现思路相对来说比较复杂:
1.首先一个Variabel的结构体变量:
typedef struct {
UINT32 WeakupTimes;
UINT32 DelayTime;
UINT32 Year;
UINT32 Month;
UINT32 Day;
UINT32 Hour;
UINT32 Minute;
UINT32 Second;
UINT8 WeakupType;
UINT8 DailyEvent;
UINT8 SingleWeakupType;
} WEAKUP_ONALARM_CONFIGURATION;
自定义一个WeakupData Variable,其中 Year,Month,Day,Hour, Minute,Second这些
Variable的变量用来存储用户输入的值,比如我们输入的值为:2021:1:27,
时间输入为14:39:00,这个时候我们就把所有的时信息存储到WeakupData这个
Variable中去了,不妨dump出来.
Tips:使用dmpstore -all 可以看到当前UEFI Shell下面所有的Variable,他们的大小和他们的值。
一目了然, 拿去用吧,不用谢我。
这时,我们发现Variable中存储的信息和我们所输入的信息是一致的。至此我们第一步已经搞定了。
2.获取用户所输入的时间和当前时间的差值。
注意:思考~
我们可不可以像UserDefine那种方式,直接拿用户输入的时间的值减去当前的时间,
得到差值之后,延时差值去唤醒系统呢?
答:不好!因为,比如说用户输入的唤醒时间比当前系统时间的差值非常小,假设只有2秒钟。
这个时候,我们的系统还没有机会进入S5,唤醒系统的时钟中断已经来了,
这样就没有办法唤醒系统了。
那么,我们该怎么办么?
这里,我想到了一种实现的方案:拿用户输入的时间值和1970年1月1日的时间作对比,
那当前的时间和1970年1月1日的时间作对比,两个差值做比较,就可以获得延时的时间了。
这里给你展示一下1970年到某年某月某日的算法:
UINTN
STATIC
MakeTime(
UINTN Year,
UINTN Month,
UINTN Day
)
{
Month -= 2;
if(Month <= 0){
Month += 12;
Year -= 1;
}
return (UINTN)(((Year / 4 - Year / 100 + Year / 400 + 367 * Month / 12 + Day)
+ Year * 365 - 719499));
}
这段代码挺有意思的,看不懂的话,欢迎和我交流。我也是花了半天的功夫搞懂了哟~,
里面也不存在"千年虫"问题.
3.完成以上两个步骤时,我们的主要功能已经完成了,但是这里有一个问题:
比如说我们如果单独写驱动的完成该功能的时候,我们应该在什么地方获取用户输入的时间,
并作出相应的处理呢?可以在CallBack函数中进行处理么?
答,不可以啊!
你可以,不在Callback函数中做处理,甚至你可以不实现CallBack函数,
但是在UEFI的驱动模型中,允许调用者从目标驱动程序中提取一个或多个
命名元素的当前配置的函数是:ExtractConfig!所以我们应该在这里去实现,
感兴趣的朋友可以在代码中添加打印信息,看看这个函数的调用的时机。
当驱动中相关的元素发生改变的时候,Driver会主动的,多次的调用这个函数,注意:是静态的!
通过以上3个案例,对uefi中VFR的语法和使用应该有个比较清晰的认知了.

本文分享了在龙芯4000和5000平台上的VFR开发经历,包括设置时间和日期、密码管理(使用新VfrCompile语法)及弱唤醒功能的实现与问题解决,强调了代码复用性和安全性提升。
2077

被折叠的 条评论
为什么被折叠?



