通讯协议

LPC协议实战

之前,有对LPC进行了初步认识与协议原理记录,这里介绍下LPC协议在UEFI中的实现逻辑。和其他串口或者并口通信协议不同,在LPC协议中使用的是MMIO,内存映射的通信方式,也是现阶段主板与EC芯片交互的核心协议。

通讯逻辑

在现代架构(如ARM、RISC-V)中,传统I/O端口通讯效率还是太低,取而代之的是内存映射I/O。
MMIO会将外设寄存器映射到内存地址空间,CPU可以像访问内存一样访问这些寄存器,提升通信效率。

通过查询EC芯片的手册,均会发现有相关PMC控制寄存器的介绍。比如以下是FIC6288手册中的记录。


还有专门寄存器介绍:

1
2
3
4
0x62 PMDIR PMC 主机数据写入寄存器
0x62 PMDOR PMC 主机数据读取寄存器
0x66 PMCMDR PMC 主机命令写入寄存器
0x66 PMSTR PMC 主机状态寄存器

我们通常都是用PM1用做电源EC控制。

所以在而HOST端,需要配置好命令和数据端口。示例如下:

1
2
3
4
5
6
7
#define  EC_SC    0x66
#define EC_DATA 0x62

//ACPI Embedded controller commands
#define SMC_READ_EC 0x80 //Read EC
#define SMC_WRITE_EC 0x81 //Write EC

一般掌握读写命令即可,还有其他常规EC控制命令:

嵌入式控制器命令 命令字节编码 场景应用
读取嵌入式控制器 (RD_EC) 0x80 主机读取数据
写入嵌入式控制器 (WR_EC) 0x81 主机写入数据
启用嵌入式控制器突发模式 (BE_EC) 0x82 防止EC处理或其他任务,强制其服务主处理器
禁用嵌入式控制器突发模式 (BD_EC) 0x83 恢复EC的默认工作模式
查询嵌入式控制器 (QR_EC) 0x84 查询引起SCI的事件号

当然,也可以自定义命令,不过需要在EC端实现相关处理逻辑。

UEFI实现逻辑

以下是飞腾平台UEFI代码中,LPC协议相关实现逻辑,仅供学习参考:

端口定义

1
2
3
#define LPC_BASE_ADDR       FixedPcdGet64 (PcdLpcBaseAddress)
#define EC_COMMAND_PORT LPC_BASE_ADDR + EC_SC
#define EC_DATA_PORT LPC_BASE_ADDR + EC_DATA
  • LPC_BASE_ADDR 是LPC的基地址,通过PCD配置,需要参考对应硬件平台手册配置。
  • EC_COMMAND_PORTEC_DATA_PORT分别是EC的命令端口和数据端口。

状态检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
STATIC UINT8 EcIbFree () {
UINT8 Status;
do {
Status = MmioRead8 (EC_COMMAND_PORT);
} while (Status & 2);
return SUCCESS;
}

STATIC UINT8 EcObFull () {
UINT8 Status;
do {
Status = MmioRead8 (EC_COMMAND_PORT);
} while (!(Status & 1));
return SUCCESS;
}
  • EcIbFree() 检查EC输入缓冲区是否空闲(IBF = Input Buffer Full)。
  • EcObFull() 检查EC输出缓冲区是否已满(OBF = Output Buffer Full)。
  • 这两个函数通过轮询状态寄存器实现同步。

命令和数据传输

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
UINT8 EcWriteCmd (UINT8 Cmd) {
EcIbFree ();
MmioWrite8 (EC_COMMAND_PORT, Cmd);
return SUCCESS;
}

UINT8 EcWriteData (UINT8 Data) {
EcIbFree ();
MmioWrite8 (EC_DATA_PORT, Data);
return SUCCESS;
}

UINT8 EcReadData (UINT8 *PData) {
*PData = MmioRead8 (EC_DATA_PORT);
EcObFull ();
*PData = MmioRead8 (EC_DATA_PORT);
return SUCCESS;
}
  • EcWriteCmd():向EC发送命令。
  • EcWriteData():向EC发送数据。
  • EcReadData():从EC读取数据。
  • 所有操作都通过 MmioRead8MmioWrite8 实现,这是基于内存映射I/O(MMIO)的方式。

内存访问函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
UINT8 EcReadMem (UINT8 Index, UINT8 *Data) {
UINT8 Cmd = SMC_READ_EC;
EcWriteCmd (Cmd);
EcWriteData (Index);
EcReadData (Data);
return SUCCESS;
}

UINT8 EcWriteMem (UINT8 Index, UINT8 Data) {
UINT8 Cmd = SMC_WRITE_EC;
EcWriteCmd (Cmd);
EcWriteData (Index);
EcWriteData (Data);
return SUCCESS;
}
  • EcReadMem():从EC内存中读取指定索引的数据。
  • EcWriteMem():向EC内存中写入数据到指定索引。
  • 使用特定的命令(如 SMC_READ_ECSMC_WRITE_EC)完成操作。

大致流程:UEFI下发操作命令-> 传输命令/数据-> 等待EC响应->读取返回数据。
HOST端一般都会抛出两个读写接口函数,如飞腾平台UEFI代码中EcReadMem()EcWriteMem()。目前这些均在
EDK2
开源框架已经实现,具体实现逻辑在DxeCore/xxxx/Ec.c中。在开发时,也可以自行修改,但需要根据具体硬件平台进行适配。

EC端实现逻辑

EC端为LPC通信的从设备,因为芯片的不同,具体实现逻辑也不同,这里以FIC6288为例,简单介绍一些必要内容。

寄存器配置

在EC端,除了配置和HOST端一样的命令端口和还需要额外的配置控制寄存器。需要激活或者配置中断类型。如下PM1控制寄存器介绍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 初始化列表
static const BYTE code FIC_SioTabInit[]=
{
// 配置使能逻辑设备 06h(KBD)
0x07 ,0x06, // 选择逻辑设备 06h(KBD)
0x70 ,0x01, // 使能逻辑设备中断 06h(KBD)
0x30 ,0x01, // 使能逻辑设备 06h(Mouse)
0x71 ,0x03,
// 配置使能逻辑设备 05h(Mouse)
0x07 ,0x05, // 选择逻辑设备 05h(Mouse)
0x70 ,0x0C, // 使能逻辑设备中断 05h(Mouse)
0x30 ,0x01, // 使能逻辑设备 05h(Mouse)
0x71 ,0x03,

// 使能逻辑设备 11h(PM1)
0x07 ,0x11, // 选择逻辑设备 11h(PM1)
0x70 ,0x00, // 清除逻辑设备中断 11h(PM1)
0x30 ,0x01, // 使能逻辑设备 11h(PM1)
// 使能逻辑设备 12h(PM2)
0x07 ,0x12, // 选择逻辑设备 12h(PM2)
0x70 ,0x00, // 清除逻辑设备中断 12h(PM2)
0x30 ,0x01, // 使能逻辑设备 12h(PM2)
// 使能逻辑设备 04h(MSWC)
0x07 ,0x04, // 选择逻辑设备 04h(MSWC)
0x30 ,0x00, // 禁能 MSWC

};

其中0x07选择逻辑设备,0x70 清除中断,0x30 使能逻辑设备。代码中使能了PM1PM2两个逻辑设备,其中PM1就是我们用来管理电源LPC通讯的链路。
逻辑设备定义如下:

1
2
3
4
5
6
7
8
9
10
/******************************************************************************
** Function name : FIC_SioInit(void)
** Descriptions : 初始化 LPC 一些参数
** input parameters : NULL
** Returned value : NULL
*****************************************************************************/
void FIC_SioInit(void)
{
FIC_PNPTableDataInit(FIC_SioTabInit, sizeof(FIC_SioTabInit));
}

中断逻辑处理

  • 中断向量配置
    在寄存器发生中断时,会根据向量表检索到响应的函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
 static  const pfn_ptr_v_v code FIC_IrqServTab[] = 
{


FIC_IrqInt24KBCIBF, // INT24 KBC input buffer empty interrupt
FIC_IrqInt25PMC1IBF, // INT25 PMC input buffer empty interrupt
FIC_IrqInt26PMC2OBE, // INT26 PMC2 Output Buffer Empty Intr.
FIC_IrqInt27PMC2IBF, // INT27 PMC2 Input Buffer Full Intr.
...
..
.
}

知道寄存器PMC1IBF 响应函数FIC_IrqInt25PMC1IBF,根据入口可以找到对应的命令处理函数。

  • 中断寄存器响应
    大致都会对收到的对应命令,分别处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

void FIC_ServPci2F(void)
{
unsigned char tempCmd ;
do {

// 命令端口
if ( PM1STS & P_C_D ) {
PM1Cmd = PM1DI;
PM1Step = 0x00;
tempCmd = PM1Cmd;

if(PM1Cmd == 0x80) {
FIC_ACPICmd80();
FIC_OEMACPICmd80();

} else if(PM1Cmd == 0x81) {
FIC_ACPICmd81();
FIC_OEMACPICmd81();

} else if(PM1Cmd == 0x82) {
FIC_ACPICmd82();
FIC_OEMACPICmd82();

} else if(PM1Cmd == 0x83) {
FIC_ACPICmd83();
FIC_OEMACPICmd83();

} else if(PM1Cmd == 0x84) {
FIC_ACPICmd84();
FIC_OEMACPICmd84();

} else {
FIC_CorePort66(PM1Cmd);
FIC_Hook66Port(PM1Cmd);
}

} else {
// 数据端口 数据加载
PM1Data = PM1DI;

if (PM1Step != 0x00) {
if(PM1Cmd == 0x80) {
FIC_ACPICmd80Data();
} else if(PM1Cmd == 0x81) {
FIC_ACPICmd81Data();
} else if(PM1Cmd == 0x82) {
FIC_ACPICmd82Data();
} else if(PM1Cmd == 0x83) {
FIC_ACPICmd83Data();
} else if(PM1Cmd == 0x84) {
FIC_ACPICmd84Data();
} else {
// 处理命令数据
FIC_CorePort62(PM1Cmd);
FIC_Hook62Port(PM1Cmd);
}
PM1Step--;
}
}
} while((IS_MASK_SET(PM1STS, BURST) || ECCheckBurstMode) && (FIC_BurstModeCheck() == 1));

TR1 = 0;
TF1 = 0;
ET1 = 1;
FIC_AllInterruptCtrl(ENABLE);
FIC_PMCIBFIntEnable();
FIC_HookACPIComm();
}

核心逻辑:区分命令端口与数据端口数据,根据不同指令码(0x80~0x84)匹配对应的处理函数,同时支持OEM自定义钩子函数,方便功能扩展。

ACPI发送数据

EC完成处理后,通过ACPI通道将结果回传给HOST主机,完成一次完整的LPC通信交互。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void FIC_ACPICmd80Data(void)
{
if (PM1Step == 1) {
FIC_SendFromACPI(FIC_HookMapECSpaceRead(PM1Data));
ECCheckBurstMode = 0;
}
}


void FIC_SendFromACPI(BYTE ecdata)
{
PM1DO = ecdata;
}

将EC处理完成的数据写入PM1输出寄存器,根据ACPI协议上报至主机,完成主机操作的闭环。

这次先分享到这,后续有更新再补充。