浅谈UEFI中VFR文件开发

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

废话不谈,通过三个实际的开发项目,分享一下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的语法和使用应该有个比较清晰的认知了.
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘德华海淀分华

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值