UEFI学习(四)-SuperIo的访问
- 一、什么是Super I/O?
- 二、我们要用SuperIo实现什么
- 三、NCT5581D的访问机制
- 配置顺序
- ①进入扩展功能模式
- ②配置寄存器
- ③退出扩展功能模式
- 目标一实现
- SIO.c
- SIO.inf
- 结果演示
- 目标二实现
- 目标三实现
- CPU温度读取
- CPU电压读取
- CPU风扇转速读取
- CPU风扇转速控制
- 目标四实现
- 个人对于目标三和四的实现
一、什么是Super I/O?
Super I/O 芯片也叫 I/O 芯片。在 486 以上档次的主板上都有 I/O控制电路。因为在南桥这样的高速设备和串行、并行接口、软盘驱动器及键盘鼠标等大量低速设备之间必定存在资源的不匹配,而需要经过转换和管理。而 Super I/O 芯片则完成了该功能。
通常在硬件监控芯片硬件监控芯片中会整合超级 I/O 功能,可用于监控受监控对象的电压、温度、转速等。主板在附件中会提供某种软件,它和主板上的硬件配合使用就能实现对电压、温度、风扇转速等的监控,一旦检测到这些参数超出设定的指标时,它会自动作出调整,以保护元件的安全。常见的温度控制芯片有 ADT7463 等等;通用的通用硬件监控芯片有 Winbond W83697HF 和 W83627HF , SMSC 的 LPC47M172 , ITE 的IT8705F、IT8703F,ASUS 的 AS99172F 等等,这些芯片通常还整合了对于温度的监控需与温度传感元件配合;对风扇电机转速的监控,则需与 CPU 的散热风扇配合使用。
SIO是一个半可定制化的芯片,怎么说是半可定制化呢?比如上电时序,这一部分就是固化好的,而可定制化部分则是逻辑设备(Logic Device)部分。接入电源后SIO便根据固化的程序开始运作,等待power button触发。按下power button后SIO开始跑上电时序,CPU Reset后BIOS才开始跑,此时BIOS给SIO配置的Logic Device也才生效。
二、我们要用SuperIo实现什么
本文使用的SuperIo为NCT5581D,需要注意的是不同的厂家SuperIo的访问方式与配置都会有差异。学习的资料为对应的datasheet和program guide。
对于NCT5581D我们的学习目标为:
- 了解NCT5581D的访问机制,学习 SIO Logic Device 相关内容。编写 一个 UEFI App(SIO.EFI),用于读写 SIO Logical Device 寄存器。
命令格式
读: SIO.EFI ldn //显示 ldn 这个 Device 下面的 256 个寄存器值
写: SIO.EFI ldn,offset,value //把 value 值写到 ldn 这个 Device,地址为 offset - 了解NCT5581D的GPIO功能,选择一个 GPIO 引脚,用 SIO.EFI 工具控制输出电平。万用表测量,验证效果
- 了解NCT5581D的Hardware Monitor的相关内容与其功能,编写 HardWareMonit 工具(HWM.efi)。实现一些功能:
1、读取 CPU 温度、风扇转速、CPU 电压。循环显示出来
2、控制 CPU 风扇转。 - 了解NCT5581D的WDT功能,编写看门狗测试工具(WDT.efi)。
运行命令:
SIO.EFI nSec //设置 WDT Timer 为 nSec 秒。 nSec 秒后系统自动重启;实时显示 Timer 剩余时间
三、NCT5581D的访问机制
若要了解NCT5581D的访问机制需要对datasheet的第7章内容进行学习,从datasheet中可以了解到NCT5581D使用一个特殊的协议来访问配置寄存器来设置不同类型的配置。NCT5581D有十多个逻辑设备(from Logical Device 1 to Logical Device 16)如果将所有逻辑设备配置寄存器映射到正常的PC地址空间,则需要很大的地址空间。所以,NCT5581D使用两个I/O地址(2Eh/2Fh或4Eh/4Fh)映射所有配置寄存器,2Eh/2Fh或4Eh/4Fh作为index(索引)/data(数据)对进行对superio内部寄存器的访问。我们可以通过,对2Eh写入地址,去选中相应寄存器;而对2Fh进行读写数据,来达到我们对superio内部寄存器的目的。需要注意的是在NCT5581D中有一组全局寄存器位于0h-2fh的索引处,其中包含了整个芯片的信息和配置。而访问单个逻辑设备的控制寄存器的方法也很简单。只需将所需的逻辑设备号写入全局寄存器07h。索引为30h或更高的后续访问将直接访问到逻辑设备寄存器。
在我们开始着手去编写应用去验证功能之前,我们还需要了解一下,NCT5581D内部寄存器的结构,以便对上述提到的内容有更好的理解。
(其中A25/A26是访问HardwareMonitor的IOspace的index/data对,后面会解释。)
下面列出的是NCT5581D的全局寄存器,也就是对应于上图中00h-ffh的寄存器
从上图中的备注中可以看到全局变量寄存器CR20h和CR21h(注:CR的意思是control register就是控制寄存器的意思)中保存的是Chip-ID的高八位和低八位。其中低八位可能会因为版本变更而有变化,但高八位总是不变的。
看到这里我们可以对NCT5581D有一个验证的设想,我们可以通过上面描述的方式对全局变量寄存器CR20h和CR21h进行读取然后对照上表进行验证是否正确获得相应寄存器的值,从而证明上述的superio访问方式是正确的可行的。
配置顺序
根据第7章的内容想要读取寄存器的值需要按照一定的配置顺序:
- Enter the Extended Function Mode.(进入扩展功能模式)
- Configure the configuration registers(配置寄存器)
- Exit the Extended Function Mode.(退出扩展功能模式)
①进入扩展功能模式
要将芯片置入扩展功能模式,必须连续两次0x87写入应用于扩展功能启用注册器(EFERs,即2Eh或4Eh)。
②配置寄存器
芯片选择逻辑设备,并通过功能索引扩展功能数据寄存器(EFIR)和扩展功能数据寄存器(EFDR)激活所需的逻辑设备。EFIR与EFER位于相同的地址,而EFDR位于地址(EFIR+1)。首先,将逻辑设备编号(即0x07)写入EFIR,然后将所需的逻辑设备的编号写入EFDR。如果要访问芯片(全局)控制注册器,则不需要执行此步骤。其次,将逻辑设备中所需的配置寄存器的地址写入EFIR,然后通过EFDR写入(或读取)所需的配置寄存器。
③退出扩展功能模式
要退出扩展功能模式,需要向EFER写入0xAA。一旦芯片退出扩展功能模式,它将处于正常运行模式,并准备进入配置模式。
至此,了解了上述内容我们便可以开始着手并去验证这个访问机制了。
- 参照UEFI学习(二),我们创建一个edkii的标准应用工程采用MdeModulePkg包来编译,文件夹名字取为mySio,包含mySio.c和mySio.inf;
- mySio.c中加入以下内容:
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiApplicationEntryPoint.h>
#include <Library/IoLib.h>
//edkii中已包含了对io地址读写的库IoLib.h,从而我们可以使用IoWrite8/IoRead8进行写操作和读操作,
//具体函数定义还需跳转到IoLib.h去查看。
#define IoIndexPort 0x2E
#define IoDataPort 0x2F
EFI_STATUS
EFIAPI
UefiMain (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
UINT8 ID_High=0;
UINT8 ID_Low=0;
//进入扩展模式
IoWrite8(IoIndexPort,0x87);
IoWrite8(IoIndexPort,0x87);
//读取全局寄存器读取ID号:高位+地位
IoWrite8(IoIndexPort,0x20);
ID_High=IoRead8(IoDataPort);
IoWrite8(IoIndexPort,0x21);
ID_Low=IoRead8(IoDataPort);
Print(L"SuperIO ID is %X %X\n",ID_High,ID_Low);
//退出扩展模式
IoWrite8(IoIndexPort,0xAA);
return EFI_SUCCESS;
}
- mySio.inf中加入以下内容:
[Defines]
INF_VERSION = 0x00010005
BASE_NAME = mySio
FILE_GUID = c3d8fe10-29fb-48bd-bf61-6fda693d7712
MODULE_TYPE = UEFI_APPLICATION
VERSION_STRING = 1.0
ENTRY_POINT = UefiMain
[Sources]
mySio.c
[Packages]
MdePkg/MdePkg.dec
MdeModulePkg/MdeModulePkg.dec
[LibraryClasses]
UefiApplicationEntryPoint
UefiLib
IoLib
- IoLib.h的路径在edk2\MdePkg\Include\Library\IoLib.h,其中我们可以从中查看到IoWrite8/IoRead8两个函数的描述而对于其底层实现,我们现在可以暂时不用去深入了解:
/**
Reads an 8-bit I/O port.
Reads the 8-bit I/O port specified by Port. The 8-bit read value is returned.
This function must guarantee that all I/O read and write operations are
serialized.
If 8-bit I/O port operations are not supported, then ASSERT().
@param Port The I/O port to read.
@return The value read.
**/
UINT8
EFIAPI
IoRead8 (
IN UINTN Port
);
/**
Writes an 8-bit I/O port.
Writes the 8-bit I/O port specified by Port with the value specified by Value
and returns Value. This function must guarantee that all I/O read and write
operations are serialized.
If 8-bit I/O port operations are not supported, then ASSERT().
@param Port The I/O port to write.
@param Value The value to write to the I/O port.
@return The value written the I/O port.
**/
UINT8
EFIAPI
IoWrite8 (
IN UINTN Port,
IN UINT8 Value
);
5.按照UEFI学习(二)将我们编写好的工程文件加入到MdeModulePkg的.dsc文件中并进行编译得到mySio.efi,将其转移到具备SHELL环境的u盘中,并在带有NCT5581D的测试机台上开机并进入shell环境,运行我们编译得到的mySio.efi文件。结果如下图所示,可以看到高八位的确如datasheet中描述的一样为default值D4h而低八位应该是因为版本更变。
目标一实现
至此,已经完成了我们的访问机制的验证。接下来我们可以开始去着手实现目标一了。对于参数的传入,我们可以通过创建shell工程应用去实现,通过学习《UEFI原理与编程》第三章的3.2.1,学习shell应用程序的特点与创建过程。接下来给大家分享一下个人对于目标一任务的实现并会在代码中作出一定解释帮助阅读者阅读。
SIO.c
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/DebugLib.h>
#include <Library/ShellCEntryLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiRuntimeServicesTableLib.h>
#include <Library/IoLib.h>
#include <Library/ShellLib.h>
#include <Library/BaseLib/BaseLibInternals.h>
/*
ShellCEntryLib.h和UefiBootServicesTableLib.h是shell应用工程需要,IoLib.h是IO读取需要,
ShellLib.h是调用ShellPrintEx所需要包含的库,用于指定坐标打印。
*/
#define IoIndexPort 0x2E
#define IoDataPort 0x2F
UINT8 symbol2=0;
UINT8 ROWNUM[16] = {0x00,0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0, 0xF0};
void StartToReadRC()//进入扩展模式
{
IoWrite8(IoIndexPort, 0x87);
IoWrite8(IoIndexPort, 0x87);
return;
}
void QuitRCReader()//推出扩展模式
{
IoWrite8(IoIndexPort, 0xAA);
return;
}
UINT8 SelectLogicalDevice(IN CHAR16* LDn)//选择逻辑设备并屏蔽部分保留的逻辑设备号,NCT5581d包含逻辑设备号0-9,A-F,10-16
{
UINT8 symbol = 0;
Print(L"The input LD is %s\n",LDn);
if(LDn[0]!='L'||LDn[1]!='D')
{
ShellPrintEx(-1, -1, L"%HTHE LD parameter Input illegal!!\n");
return symbol;
}
switch (LDn[2])
{
case '0':Print(L"This LD is reserved!\n"); break;
case '1':if(LDn[3]=='6')//case 1的特殊是因为10-16的逻辑设备中只有16是有用的其余为保留
{IoWrite8(IoIndexPort, 0x07); IoWrite8(IoDataPort, 0x16); symbol = 1;}
else
{Print(L"This LD is reserved!\n"); }
break;
case '2':IoWrite8(IoIndexPort, 0x07); IoWrite8(IoDataPort, 0x02); symbol = 1;break;
case '3':IoWrite8(IoIndexPort, 0x07); IoWrite8(IoDataPort, 0x03); symbol = 1;break;
case '4':Print(L"This LD is reserved!\n"); break;
case '5':IoWrite8(IoIndexPort, 0x07); IoWrite8(IoDataPort, 0x05); symbol = 1;break;
case '6':IoWrite8(IoIndexPort, 0x07); IoWrite8(IoDataPort, 0x06); symbol = 1;break;
case '7':IoWrite8(IoIndexPort, 0x07); IoWrite8(IoDataPort, 0x07); symbol = 1;break;
case '8':IoWrite8(IoIndexPort, 0x07); IoWrite8(IoDataPort, 0x08); symbol = 1;break;
case '9':IoWrite8(IoIndexPort, 0x07); IoWrite8(IoDataPort, 0x09); symbol = 1;break;
case 'A':IoWrite8(IoIndexPort, 0x07); IoWrite8(IoDataPort, 0x0A); symbol = 1;break;
case 'B':IoWrite8(IoIndexPort, 0x07); IoWrite8(IoDataPort, 0x0B); symbol = 1;break;
case 'C':Print(L"This LD is reserved!\n"); break;
case 'D':IoWrite8(IoIndexPort, 0x07); IoWrite8(IoDataPort, 0x0D); symbol = 1;break;
case 'E':IoWrite8(IoIndexPort, 0x07); IoWrite8(IoDataPort, 0x0E); symbol = 1;break;
case 'F':IoWrite8(IoIndexPort, 0x07); IoWrite8(IoDataPort, 0x0F); symbol = 1;break;
default:ShellPrintEx(-1, -1, L"%HInput illegal!!\n");break;
}
return symbol;//symbol是为了判断是否成功
}
void ReadRC()//表头与内容打印的实现
{
UINT8 RCvalue = 0,Times=0;
INT32 colinit=0, rowinit=0;
INT32 col=0, row=0,a=1,b=17;
//Print(L"The row's value is %d!\n",gST->ConOut->Mode->CursorRow);
Print(L"\n");
colinit=gST->ConOut->Mode->CursorColumn;
rowinit=gST->ConOut->Mode->CursorRow;
//打印行表头
ShellPrintEx(colinit, rowinit, L"%H 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F");
for (row =rowinit+a; row < rowinit+b; row++)
{
col = 0;
ShellPrintEx(col, row,L"%H%02x", ROWNUM[row - 1-rowinit]);//打印列表头
for (col = 5; col < 66;)
{
IoWrite8(IoIndexPort, Times);
RCvalue= IoRead8(IoDataPort);
ShellPrintEx(col, row, L"%B%02x",RCvalue);
Times++;
col += 4;
}
}
if (Times==0)//unit8最大为255,而打印结束后Times已经增加到256即溢出为0了,而这一步是为了验证是否打印完全并正确
{
ShellPrintEx(0, rowinit+19, L"%HRCread is complete!\n");
}
else
{
ShellPrintEx(0,rowinit+ 19, L"%HRCread error!\n");
Print(L"The Times's value is %d!\n",Times);
}
return;
}
UINT8 Transfer(IN CHAR16 value)
{
UINT8 number=0;
if(value>47&&value<58)
number=(UINT8)(value-48);
else if(value>64&&value<71)
number=(UINT8)(value-55);
else if(value>96&&value<103)
number=(UINT8)(value-87);
else
{Print(L"The format of the parameter value is incorrect!\n");symbol2=1;}
return number;
}
INTN
EFIAPI
ShellAppMain(
IN UINTN Argc,
IN CHAR16** Argv
)
{
UINT8 offsetRT=0;
UINT8 valueRT=0;
//通过判断输入参数的个数来选择不同的功能
switch (Argc)
{
case 1:Print(L"Argv[1] = NULL\n"); break;
case 2:StartToReadRC();
if(SelectLogicalDevice(Argv[1]))//?1
ReadRC();
else
Print(L"Select Logical Device Fail!\n");
QuitRCReader();
break;
case 3:Print(L"Missing Parameter!\n"); break;
case 4:StartToReadRC();
if(SelectLogicalDevice(Argv[1]))
{
offsetRT=Transfer(Argv[2][0])*16+Transfer(Argv[2][1]);
Print(L"offsetRT is %x!\n",offsetRT);
if(symbol2)
break;
else
{
valueRT=Transfer(Argv[3][0])*16+Transfer(Argv[3][1]);
Print(L"valueRT is %x!\n",valueRT);
if(symbol2)
break;
else
{IoWrite8(IoIndexPort,offsetRT );
IoWrite8(IoDataPort,valueRT );break;}
}
}
QuitRCReader();
break;
default:Print(L"Not Ready Yet!\n");
break;
}
return 0;
}
SIO.inf
[Defines]
INF_VERSION = 0x00010006
BASE_NAME = SIO
FILE_GUID = 7a6ca3b8-ee1b-489c-b300-24544a7bd418
MODULE_TYPE = UEFI_APPLICATION
VERSION_STRING = 1.0
ENTRY_POINT = ShellCEntryLib
[Sources]
SIO.c
[Packages]
MdePkg/MdePkg.dec
ShellPkg/ShellPkg.dec
MdeModulePkg/MdeModulePkg.dec
[LibraryClasses]
ShellCEntryLib
UefiLib
IoLib
ShellLib
结果演示
目标二实现
对于目标二的实现,首先需要对NCT5581D的GPIO具有一定的了解,掌握其配置所需要用到的寄存器。在datasheet的第17章中有对GPIO的详细介绍,The NCT5581D provides input/output ports that can be individually configured to perform a simple basic I/O function or alternative, pre-defined function. Users can configure each individual port to be an input or output port by programming respective bit in selection register (0 = output, 1 = input). Invert port value by setting inversion register (0 = non–inverse, 1 = inverse). Port value is read/write through data register.
而要实现具体的配置,以GP84为例,下面表中已经提示了要想实现GP84的GPIO功能需要先对其进行使能,配置相关的寄存器使GP84达到输入还是输出,极性等等。
- 首先是Logical Device 7 CR30h将GPIO8置active
- 接下来需要注意的是在旧版的datasheet中可能很难查到的GPIO8使能位的设置,该设置位于全局变量寄存器CR2Ah中
- 接下来要将GP84设置成需要的输入还是输出模式,电平极性是否需要反转,设置电平以及状态寄存器的读取以及功能复用的选择寄存器的设置寄存器位于 Logic Device 7, E4~E7 and ED
- 设置GPIO上拉还是开漏模式,寄存器位于 Logic Device F, CRE7
了解了需要配置的寄存器,我们就可以开始实现目标二的要求了。因为是要使用万用表进行测量验证,所以在观察了原理图之后选择了GP84这个口,设想是将其配置GPIO输出模式,不用翻转极性,通过设置电平后用状态寄存器和万用表去验证功能,没有万用表时:在设置好相应寄存器后先让GPIO84清除状态寄存器,然后让GPIO84输出低电平再让GPIO84输出高电平,此时在状态寄存器的相应位可以读取到状态寄存器侦测到一个电平跳变,从而验证我们的设置是正确的。
下面列出我的GPIO.c:
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/DebugLib.h>
#include <Library/ShellCEntryLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiRuntimeServicesTableLib.h>
#include <Library/IoLib.h>
#include <Library/ShellLib.h>
#include <Library/BaseLib/BaseLibInternals.h>
#define IoIndexPort 0x2E
#define IoDataPort 0x2F
INTN
EFIAPI
ShellAppMain(
IN UINTN Argc,
IN CHAR16** Argv
)
{
UINT8 GPIO84status=0;
UINT8 GPIO84status2=0;
//enter extern mode
IoWrite8(IoIndexPort, 0x87);
IoWrite8(IoIndexPort, 0x87);
//select logical device 07 to enable gpio84
IoWrite8(IoIndexPort, 0x07);
IoWrite8(IoDataPort, 0x07);
IoWrite8(IoIndexPort, 0x30);
IoWrite8(IoDataPort, 0x04);
GPIO84status2=(IoRead8(IoDataPort)>>2)&0x01;
ShellPrintEx(-1, -1, L"%B%H The cr30[2] is %x\n",GPIO84status2);
//SET function to GPIO
IoWrite8(IoIndexPort, 0x2A);
GPIO84status2=IoRead8(IoDataPort)|0x80;
IoWrite8(IoIndexPort, 0x2A);
IoWrite8(IoDataPort, GPIO84status2);
GPIO84status2=(IoRead8(IoDataPort)>>7)&0x01;
ShellPrintEx(-1, -1, L"%B%H The cr2A[8] is %x\n",GPIO84status2);
// set gpio84 output
IoWrite8(IoIndexPort, 0xE4);
IoWrite8(IoDataPort, 0xEF);
GPIO84status2=(IoRead8(IoDataPort)>>4)&0x01;
ShellPrintEx(-1, -1, L"%B%H The crE4[4] is %x\n",GPIO84status2);
//set GPIO84 Push-Pull
IoWrite8(IoIndexPort, 0x07);
IoWrite8(IoDataPort, 0x0F);
IoWrite8(IoIndexPort, 0xE7);
IoWrite8(IoDataPort, 0xEF);
//set gpio84 output highlevel
IoWrite8(IoIndexPort, 0x07);
IoWrite8(IoDataPort, 0x07);
IoWrite8(IoIndexPort, 0xE5);
IoWrite8(IoDataPort, 0x10);
GPIO84status2=(IoRead8(IoDataPort)>>4)&0x01;
ShellPrintEx(-1, -1, L"%B%H The GPIO84status1 is %x\n",GPIO84status2);
//read gpio8 status register to clear status
IoWrite8(IoIndexPort, 0xE7);
IoRead8(IoDataPort);
//set GPIO84 OUT OF Push-Pull
IoWrite8(IoIndexPort, 0x07);
IoWrite8(IoDataPort, 0x0F);
IoWrite8(IoIndexPort, 0xE7);
IoWrite8(IoDataPort, 0xFF);
//set gpio84 output lowlevel
IoWrite8(IoIndexPort, 0x07);
IoWrite8(IoDataPort, 0x07);
IoWrite8(IoIndexPort, 0xE5);
IoWrite8(IoDataPort, 0x00);
GPIO84status2=(IoRead8(IoDataPort)>>4)&0x01;
ShellPrintEx(-1, -1, L"%B%H The GPIO84status2 is %x\n",GPIO84status2);
//set GPIO84 Push-Pull
IoWrite8(IoIndexPort, 0x07);
IoWrite8(IoDataPort, 0x0F);
IoWrite8(IoIndexPort, 0xE7);
IoWrite8(IoDataPort, 0xEF);
//set gpio84 output highlevel
IoWrite8(IoIndexPort, 0x07);
IoWrite8(IoDataPort, 0x07);
IoWrite8(IoIndexPort, 0xE5);
IoWrite8(IoDataPort, 0x10);
GPIO84status2=(IoRead8(IoDataPort)>>4)&0x01;
ShellPrintEx(-1, -1, L"%B%H The GPIO84status3 is %x\n",GPIO84status2);
IoWrite8(IoIndexPort, 0xE7);
GPIO84status=IoRead8(IoDataPort);
ShellPrintEx(-1, -1, L"%B%H The E7status is %x\n",GPIO84status2);
GPIO84status=(GPIO84status>>4)&0x01;
if(GPIO84status)
ShellPrintEx(-1, -1, L"%B%H GPIO Setting Success!");
else
ShellPrintEx(-1, -1, L"%B%H GPIO Setting Fail!");
IoWrite8(IoIndexPort, 0xAA);
return 0;
}
可以看到,我的GPIO.c就是按照前面提到的配置去设置好相应的寄存器,就可以实现这个功能。
目标三实现
NCT5581D 的Hardware Monitor 监控PC硬件中的几个关键参数,包括电源电压、风扇速度和温度,所有这些对于高端计算机系统稳定和正常工作都是非常重要的。此外,专有的硬件减少了编程和处理器干预的数量,以控制冷却风扇的速度,最小化环境噪音,最大化系统可靠性。基于Hardware Monitor功能的强大和复杂性,单单前文提到的逻辑设备上的寄存器是远远不够的,所以NCT5581D为其提供了一个block去存放Hardware Monitor功能配置所需要的众多寄存器,那么问题就来了,对于这个block我们要怎么去访问呢?从datasheet的第八章我们可以看到,“NCT5581D提供了两个接口,LPC和I2C,供微处理器读取或写硬件监视器的内部寄存器。”而对于从LPC的访问方式是与前文提到的index/data对的IO访问方式是相似的,只不过前文使用的是2E/2F,而Hardware Monitor需要用到另外的端口号。在datasheet中同样提到了Hardware Monitor所用到的索引/数据对的基地址由使用超级I/O协议访问CR[60h]和CR[61h]获得。即想要获取Hardware Monitor需要用到的索引/数据对,我们先要选中Hardware Monitor对应的逻辑设备号,并读取其CR[60h]和CR[61h]的值,使用我们前面实现的打印功能,我们可以很容易的获取到这两个寄存器的值。分别为0x0A和0x20,但是需要注意的是,此时获取的仅仅只是基地址,而且基地址要由这两个寄存器怎么转换得到我们尚未可知,当我们继续往下看datasheet,在第九章的开头我们可以看到“地址端口和数据端口的基本地址在硬件监视设备逻辑设备B的寄存器CR60h和CR61h中指定。60h是高字节,61h是低字节。地址端口和数据端口分别位于基本地址,再加上5小时和6小时。例如,如果CR[60h]为02h,CR[61h]为90h,则地址端口为0x295h,数据端口为0x296h。”所以我们根据读到的CR[60h]和CR[61h]的值我们可以求得Hardware Monitor所用到的索引/数据对应该为A25/A26;至此,我们可以按此去访问Hardware Monitor的专属block了(通常把这种block称为IOspace)。
另外,第八章内容中还提到了“由于内部登记器的数量,有必要将登记器集划分为登记器4Eh指定的“bank”。”对此,我的个人理解是对前面选择逻辑设备号的一种套娃方式,把众多寄存器进行bank的分区,通过固定的寄存器去选择相应bank再去选择bank中的寄存器从而减少地址数量的需求。而对于选择bank则是在bank0的CR4E,该寄存器的描述为:
读到这里的时候,我当初会产生很多疑问,就是当我已经跳转到其他bank之后,我要怎么再选择bank0的4E去实现跳转到其他bank?难道每个bank的4e寄存器都可以实现跳转?那为什么datasheet要强调bank选择寄存器位于bank0的4E呢?而当我查阅了datasheet的第九章Hardware Monitor的众多寄存器后我发现,只有bank0具有4E寄存器,那么我的理解就是,在其他bank的4E寄存器的位置都是bank04E寄存器的映射,所以当我们跳转到其他bank后我们对该bank的4E进行写入便可实现对bank0的4E寄存器的值的修改,从而实现跳转。
掌握了对Hardware Monitor IOSpace的访问机制后,我们接下来就是要像配置GPIO功能一样去理解目标三中提到的功能实现的相关寄存器的配置。
CPU温度读取
对于CPU温度的读取需要理解PECI接口,从第八章内容我们可以理解相关的内容,“PECI(平台环境控制接口)是一种新的数字接口,可以读取Intel®CPU的CPU温度。PECI的带宽从2Kbps到2Mbps,它使用一根线进行自时钟和数据传输。通过与Intel®CPU中的数字热传感器(DTS)接口,PECI报告相对于激活热控制电路(TCC)的处理器温度的负温度(计数)。在TCC激活温度下,英特尔CPU将以降低性能运行,以防止设备发生热损坏。”
而读取的配置顺序为:
- BIOS have to set Global Control Register CR2C bit 0 = 0 to select Pin function as PECI.
- Set Bank 7 Index 03h = 10h for Agent 0 and Domain 0
- Set Bank 7 Index 09h for Agent 0 Tbase
- Set Bank 7 Index 01h = 95h to enable PECI3.0
- Get temperature raw data from Bank 7 Index 17h (high byte) / 18h (low byte)
- Set Bank 2 Index 00h = 10h to select PECI Agent 0 as CPUFAN monitoring source
- Set Bank 0 Index AEh bit 0 to 1 to enable PECI Agent 0 mode
- PECI Temperature Reading
对于上述配置中的Tbase的配置和 raw data的配置可以从步骤8图中得到相应的关系,这两个步骤的意义在于当温度与我们自己测量到的温度有很大偏差时作调整用,而我们现在是为了练习对superio的访问,所以我们可以不对这两个步骤进行配置修改。以下是我的在CPU温度读取的代码实现:
UINTN ReadCPUTemperature()
{
UINTN CPUTemperature=0;
//set Global Control Register CR2C bit 0 = 0 to select Pin function as PECI.
IoWrite8(IoIndexPort, 0x2C);
IoWrite8(IoDataPort, 0x00);
//Set Bank 7 Index 03h = 10h for Agent 0 and Domain 0
IoWrite8(baseAddress+0x05, 0x4E);
IoWrite8(baseAddress+0x06, 7);
IoWrite8(baseAddress+0x05, 0x03);
IoWrite8(baseAddress+0x06, 0x10);
//Set Bank 7 Index 01h = 95h to enable PECI3.0
IoWrite8(baseAddress+0x05, 0x01);
IoWrite8(baseAddress+0x06, 0x95);
//Set Bank 2 Index 00h = 10h to select PECI Agent 0 as CPUFAN monitoring source
IoWrite8(baseAddress+0x05, 0x4E);
IoWrite8(baseAddress+0x06, 2);
IoWrite8(baseAddress+0x05, 0x00);
IoWrite8(baseAddress+0x06, 0x10);
//Set Bank 0 Index AEh bit 0 to 1 to enable PECI Agent 0 mode
IoWrite8(baseAddress+0x05, 0x4E);
IoWrite8(baseAddress+0x06, 0);
IoWrite8(baseAddress+0x05, 0xAE);
IoWrite8(baseAddress+0x06, 0x01);
//Get temperature raw data from Bank 7 Index 20h
IoWrite8(baseAddress+0x05, 0x4E);
IoWrite8(baseAddress+0x06, 7);
IoWrite8(baseAddress+0x05, 0x20);
CPUTemperature= IoRead8(baseAddress+0x06);
return CPUTemperature;
}
CPU电压读取
对于CPU电压的读取很简单,只需要读取相应的寄存器就能获得当前电压值,需要注意的是需要对读取到的数值进行转换才能得到正确值,即Detected Voltage = Reading * 0.008 V
UINTN ReadCPUVoltage()
{
UINTN CPUVoltage=0;
IoWrite8(baseAddress+0x05, 0x4E);
IoWrite8(baseAddress+0x06, 4);
IoWrite8(baseAddress+0x05, 0x80);
CPUVoltage=IoRead8(baseAddress+0x06);
return CPUVoltage*8;//此处不作除法运算是会丢失精度小数的实现在下面给出
}
//实现小数显示:将整数和小数部分分开显示,加上标点符号
void Specialtreatment(UINTN CPUVoltage)
{
UINTN IntegerPart=0,FractionalPart=0;
IntegerPart=CPUVoltage/1000;
if (IntegerPart<10)
ShellPrintEx(18,1,L"%d",IntegerPart);
else if (IntegerPart<100)
{
ShellPrintEx(17,1,L"%d",IntegerPart);
ShellPrintEx(25,1,L"This is impossible!!!!");
}
FractionalPart=CPUVoltage%1000;
ShellPrintEx(20,1,L"%03d",FractionalPart);
return;
}
CPU风扇转速读取
对于CPU风扇转速的读取同样是简单的,而NCT5581D提供了两种方式去读取转速值
我采用的是第一种方式:
UINTN ReadCPUFanRPM()
{
UINTN CPUFanRPM=0;
IoWrite8(baseAddress+0x05, 0x4E);
IoWrite8(baseAddress+0x06, 4);
IoWrite8(baseAddress+0x05, 0xb2);
CPUFanRPM=IoRead8(baseAddress+0x06);
IoWrite8(baseAddress+0x05, 0xb3);
CPUFanRPM=CPUFanRPM*32+((IoRead8(baseAddress+0x06)<<3)>>3);//根据上图说明读取13bit的数值
return 1350000/CPUFanRPM;
}
CPU风扇转速控制
CPU风扇转速控制如下图所示,其中输出模式上拉还是开漏以及pwm的输出频率因为尚未对此功能进行了解,所以我让它保持原来的值,不作修改。
而对应的真实输出的占空比关系如下图:
//Select bank2 index02 bit[7:4]->00000010=02h
IoWrite8(baseAddress+0x05, 0x4E);
IoWrite8(baseAddress+0x06, 2);
IoWrite8(baseAddress+0x05, 0x02);
IoWrite8(baseAddress+0x06, 0x02);
//set PWM output(Duty)bank2 index09 bit[7:0] 00-FF
fanSpeedDuty=Transfer(Argv[3][0])*16+Transfer(Argv[3][1]);//将从命令行获取的参数进行转换成目标占空比输入值
ShellPrintEx(-1,-1,L"%H%2X",fanSpeedDuty);
IoWrite8(baseAddress+0x05, 0x09);
IoWrite8(baseAddress+0x06, fanSpeedDuty);
目标四实现
WATCH DOG TIMER的工作原理很简单,当开启了WATCH DOG功能后向计数器寄存器写入相应值,计时器便会进行倒数,当数值没有被打断递减而减为0后,会向相应位置触发一个低电平跳变,当硬件连接将重启时序连入,便可实现机器的重启。
去配置一个WATCH DOG TIMER以WDT1为例需要进行以下配置:
个人对于目标三和四的实现
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/DebugLib.h>
#include <Library/ShellCEntryLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiRuntimeServicesTableLib.h>
#include <Library/IoLib.h>
#include <Library/ShellLib.h>
#include <Library/BaseLib/BaseLibInternals.h>
#define IoIndexPort 0x2E
#define IoDataPort 0x2F
UINTN baseAddress=0;
void StartToReadRC()
{
IoWrite8(IoIndexPort, 0x87);
IoWrite8(IoIndexPort, 0x87);
return;
}
UINTN ReadCPUTemperature()
{
UINTN CPUTemperature=0;
//set Global Control Register CR2C bit 0 = 0 to select Pin function as PECI.
IoWrite8(IoIndexPort, 0x2C);
IoWrite8(IoDataPort, 0x00);
//Set Bank 7 Index 03h = 10h for Agent 0 and Domain 0
IoWrite8(baseAddress+0x05, 0x4E);
IoWrite8(baseAddress+0x06, 7);
IoWrite8(baseAddress+0x05, 0x03);
IoWrite8(baseAddress+0x06, 0x10);
//Set Bank 7 Index 01h = 95h to enable PECI3.0
IoWrite8(baseAddress+0x05, 0x01);
IoWrite8(baseAddress+0x06, 0x95);
//Set Bank 2 Index 00h = 10h to select PECI Agent 0 as CPUFAN monitoring source
IoWrite8(baseAddress+0x05, 0x4E);
IoWrite8(baseAddress+0x06, 2);
IoWrite8(baseAddress+0x05, 0x00);
IoWrite8(baseAddress+0x06, 0x10);
//Set Bank 0 Index AEh bit 0 to 1 to enable PECI Agent 0 mode
IoWrite8(baseAddress+0x05, 0x4E);
IoWrite8(baseAddress+0x06, 0);
IoWrite8(baseAddress+0x05, 0xAE);
IoWrite8(baseAddress+0x06, 0x01);
//Get temperature raw data from Bank 7 Index 20h
IoWrite8(baseAddress+0x05, 0x4E);
IoWrite8(baseAddress+0x06, 7);
IoWrite8(baseAddress+0x05, 0x20);
CPUTemperature= IoRead8(baseAddress+0x06);
return CPUTemperature;
}
UINTN ReadCPUVoltage()
{
UINTN CPUVoltage=0;
IoWrite8(baseAddress+0x05, 0x4E);
IoWrite8(baseAddress+0x06, 4);
IoWrite8(baseAddress+0x05, 0x80);
CPUVoltage=IoRead8(baseAddress+0x06);
return CPUVoltage*8;
}
UINTN ReadCPUFanRPM()
{
UINTN CPUFanRPM=0;
IoWrite8(baseAddress+0x05, 0x4E);
IoWrite8(baseAddress+0x06, 4);
IoWrite8(baseAddress+0x05, 0xb2);
CPUFanRPM=IoRead8(baseAddress+0x06);
IoWrite8(baseAddress+0x05, 0xb3);
CPUFanRPM=CPUFanRPM*32+((IoRead8(baseAddress+0x06)<<3)>>3);
return 1350000/CPUFanRPM;
}
UINTN ReadSYSTemperature()
{
UINTN SYSTemperature=0;
IoWrite8(baseAddress+0x05, 0x4E);
IoWrite8(baseAddress+0x06, 0);
IoWrite8(baseAddress+0x05, 0xAE);
if(IoRead8(baseAddress+0x06)!=0)
IoWrite8(baseAddress+0x06, 0);
IoWrite8(baseAddress+0x05, 0x4E);
IoWrite8(baseAddress+0x06, 6);
IoWrite8(baseAddress+0x05, 0x22);
IoWrite8(baseAddress+0x06, 0x02);
IoWrite8(baseAddress+0x05, 0x4E);
IoWrite8(baseAddress+0x06, 1);
IoWrite8(baseAddress+0x05, 0x50);
SYSTemperature=IoRead8(baseAddress+0x06);
IoWrite8(baseAddress+0x05, 0x51);
SYSTemperature=SYSTemperature*2+(IoRead8(baseAddress+0x06)>>7);
SYSTemperature=SYSTemperature/2;
return SYSTemperature;
}
void Specialtreatment(UINTN CPUVoltage)
{
UINTN IntegerPart=0,FractionalPart=0;
IntegerPart=CPUVoltage/1000;
if (IntegerPart<10)
ShellPrintEx(18,1,L"%d",IntegerPart);
else if (IntegerPart<100)
{
ShellPrintEx(17,1,L"%d",IntegerPart);
ShellPrintEx(25,1,L"This is impossible!!!!");
}
FractionalPart=CPUVoltage%1000;
ShellPrintEx(20,1,L"%03d",FractionalPart);
return;
}
UINT8 Transfer(IN CHAR16 value)
{
UINT8 number=0;
if(value>47&&value<58)
number=(UINT8)(value-48);
else if(value>64&&value<71)
number=(UINT8)(value-55);
else if(value>96&&value<103)
number=(UINT8)(value-87);
else
Print(L"The format of the parameter value is incorrect!\n");
return number;
}
UINT8 isNumber(IN CHAR16 value)
{
if(value>47&&value<58)
return 1;
else
return 0;
}
// void GPIOinit()
// {
// //select logical device 09 to enable gpio24
// IoWrite8(IoIndexPort, 0x07);
// IoWrite8(IoDataPort, 0x09);
// IoWrite8(IoIndexPort, 0x30);
// IoWrite8(IoDataPort, 0x01);
// //SET function to GPIO
// IoWrite8(IoIndexPort, 0x1B);
// IoWrite8(IoDataPort, IoRead8(IoDataPort)&EF);
// IoWrite8(IoIndexPort, 0x27);
// IoWrite8(IoDataPort, IoRead8(IoDataPort)&F7);
// // set gpio24 output
// IoWrite8(IoIndexPort, 0x07);
// IoWrite8(IoDataPort, 0x09);
// IoWrite8(IoIndexPort, 0xE0);
// IoWrite8(IoDataPort, IoRead8(IoDataPort)&EF);
// return ;
// }
//同样通过检测命令行参数个数分为不同的功能
INTN
EFIAPI
ShellAppMain(
IN UINTN Argc,
IN CHAR16** Argv
)
{
UINTN counter = 0;
UINTN CPUTemperature=0;
UINTN CPUVoltage=0;
UINTN CPUFanRPM=0;
UINTN SYSTemperature=0;
UINT8 fanSpeedDuty=0;
UINT8 num=0;
UINTN sec=0;
if (Argc<=1)
{
Print(L"The input parament is not right!");
return EFI_SUCCESS;
}
else if(Argv[1][0]=='D')//D代表display即显示CPU温度、电压、风扇转速
{
//extern mode
StartToReadRC();
//read logical device B 60h and 61h
IoWrite8(IoIndexPort, 0x07);
IoWrite8(IoDataPort, 0x0B);
IoWrite8(IoIndexPort, 0x60);
baseAddress=IoRead8(IoDataPort);
IoWrite8(IoIndexPort, 0x61);
baseAddress=baseAddress*256+IoRead8(IoDataPort);
gST->ConOut->ClearScreen (gST->ConOut);
ShellPrintEx(0,0,L"CPU Temperature:");//16
ShellPrintEx(0,1,L"CPU Voltage:");
ShellPrintEx(0,2,L"CPU Fan RPM:");
ShellPrintEx(0,3,L"SYS Temperature:");
ShellPrintEx(19,1,L".");
while(counter<12580)//循环显示12580次
{
ShellPrintEx(18,0,L" ");
CPUTemperature=ReadCPUTemperature();
ShellPrintEx(18,0,L"%d",CPUTemperature);
ShellPrintEx(18,1,L" . ");
CPUVoltage=ReadCPUVoltage();
Specialtreatment(CPUVoltage);
ShellPrintEx(18,2,L" ");
CPUFanRPM=ReadCPUFanRPM();
ShellPrintEx(18,2,L"%d",CPUFanRPM);
ShellPrintEx(18,3,L" ");
SYSTemperature=ReadSYSTemperature();
ShellPrintEx(18,3,L"%d",SYSTemperature);
counter++;
}
gST->ConOut->ClearScreen (gST->ConOut);
return EFI_SUCCESS;
}
else if(Argv[1][0]=='C')//C是风扇转速控制
{
//extern mode
StartToReadRC();
//read logical device B 60h and 61h
IoWrite8(IoIndexPort, 0x07);
IoWrite8(IoDataPort, 0x0B);
IoWrite8(IoIndexPort, 0x60);
baseAddress=IoRead8(IoDataPort);
IoWrite8(IoIndexPort, 0x61);
baseAddress=baseAddress*256+IoRead8(IoDataPort);
//configuration registers CR24 bit3(keep)
//configuration registers outputfrequency bank0,index02(keep)
//Fan Control Mode Select bank2 index02 bit[7:4]
if(Argc<4||(Argv[2][0]!='M'&& Argv[2][0]!='S'&& Argv[2][0]!='W'))
{
Print(L"The input parament to select fan mode and control fan speed is not right!");
return EFI_SUCCESS;
}
else if(Argv[2][0]=='M')
{
//Select bank2 index02 bit[7:4]->00000010=02h
IoWrite8(baseAddress+0x05, 0x4E);
IoWrite8(baseAddress+0x06, 2);
IoWrite8(baseAddress+0x05, 0x02);
IoWrite8(baseAddress+0x06, 0x02);
//set PWM output(Duty)bank2 index09 bit[7:0] 00-FF
fanSpeedDuty=Transfer(Argv[3][0])*16+Transfer(Argv[3][1]);
ShellPrintEx(-1,-1,L"%H%2X",fanSpeedDuty);
IoWrite8(baseAddress+0x05, 0x09);
IoWrite8(baseAddress+0x06, fanSpeedDuty);
return EFI_SUCCESS;
}
else if(Argv[2][0]=='S')//这里是后面实现SmartFan需要
{
//Trying
return EFI_SUCCESS;
}
else
return EFI_SUCCESS;
}
else if(Argv[1][0]=='W')//W代表看门狗实现
{
//
while(isNumber(Argv[2][num]))
{
sec=sec*10+(Argv[2][num]-48);
num++;
}
ShellPrintEx(-1,-1,L"setting second is :%H%d\n",sec);
if(sec>255)
{
ShellPrintEx(-1,-1,L"setting second :%H%d is too big!Change the unit to make it!\n",sec);
return EFI_SUCCESS;
}
StartToReadRC();
IoWrite8(IoIndexPort, 0x07);
IoWrite8(IoDataPort, 0x0D);
IoWrite8(IoIndexPort, 0xF0);
IoWrite8(IoDataPort, 0x00);
IoWrite8(IoIndexPort, 0x07);
IoWrite8(IoDataPort, 0x08);
//active WDT1
IoWrite8(IoIndexPort, 0x30);
IoWrite8(IoDataPort,(IoRead8(IoDataPort)&0xEF)|0X01);
//set second mode
IoWrite8(IoIndexPort, 0xF5);
IoWrite8(IoDataPort,(IoRead8(IoDataPort)&0xF7)|0x02);
IoWrite8(IoIndexPort, 0xF8);//WDT3
IoWrite8(IoDataPort,(IoRead8(IoDataPort)&0xF6)|0x02);
//set count down value
IoWrite8(IoIndexPort, 0xF6);
IoWrite8(IoDataPort, (UINT8)sec);
IoWrite8(IoIndexPort, 0xF9);//WDT3
IoWrite8(IoDataPort, (UINT8)sec);
//read count down value
gST->ConOut->ClearScreen (gST->ConOut);
ShellPrintEx(0,0,L"WDT1 is counting down: s");
// ShellPrintEx(0,1,L"WDT3 is counting down: s");
while(1)
{
ShellPrintEx(21,0,L" ");
IoWrite8(IoIndexPort, 0xF6);
ShellPrintEx(21,0,L"%H%d",IoRead8(IoDataPort));
// ShellPrintEx(21,1,L" ");
// IoWrite8(IoIndexPort, 0xF9);
// ShellPrintEx(21,1,L"%H%d",IoRead8(IoDataPort));
}
return EFI_SUCCESS;
}
else
{
Print(L"The input parament is not right!");
return EFI_SUCCESS;
}
}
至此,对NCT5581D的读取写入操作有了一定的了解,也是对edk2的一些应用的了解。
路漫漫其修远兮吾将上下而求索!