如何在UEFI环境下使用 UEFI规范提供的接口

目录
7.1 访问 pci/pcie 设备
7.1.1 与 pci/pcie 设备通信的机制
7.1.2 支持访问 pci/pcie 设备的 protocol
7.1.3 访问 pci/pcie 设备示例
进行项目开发、构建产品框架的时候,最开始需要考虑的就是采用哪种通信方式让软件可以访问外部设备(简称外设)。计算机经过多年的发展,提供了非常丰富的通信协议供程序员选择。从古老的串口协议,到使用广泛的 pci/pcie 协议,再到现在无处不在的 usb 协议等,可供选择的方式实在太多。
在 legacy bios 下,第三方开发者编写访问外设的代码是件非常辛苦的事情。主要原因在于,legacy bios 对很多协议支持得并不好。以笔者常用的 smbus 协议为例,直到现在,也没有 bios 厂家提供标准的中断接口。大多数时候,只能通过阅读主板芯片组规格书,了解与 smbus 协议相关的寄存器信息,再根据标准的 smbus 总线读写协议编写代码。市场上主板芯片组太多,这种方法写出来的代码,兼容性、稳定性都不理想,并且工作量非常大。
uefi bios 的出现,解决了上述这些问题。从 uefi 标准和 edk2 的源码也可以看出,常用的总线协议如 pci/pcie、smbus、串口等,uefi 都已经提供了支持。对于依赖于 bios 接口进行产品开发的厂商来说,这是一个非常好的消息,产品的开发速度将大幅提高,稳定性和兼容性也能得到保障。
本章将介绍如何在 uefi 环境下使用 uefi 规范提供的接口(即各类 protocol),通过 pci/pcie、smbus 和串口访问外设。
7.1 访问 pci/pcie 设备
pci(peripheral component interconnect)是一种高速的局部总线。其主要目的是连接周边设备,将低速的设备与高速的处理器结合起来,以解决用户对数据传输速率越来越高的要求。pcie 总线是在 pci 总线上继承发展来的,其将信号传输方式从并行改为了串行, 传输速率也突飞猛进,pci 的理论带宽为 133mb/s,而 pcie4.0 x16 的带宽达到了 64gb/s。
从硬件结构角度看,pci 和 pcie 有很大的不同。pci 总线采用并行总线结构,而 pcie 总线使用了高速差分总线结构,使用端到端的连接方式,这使得两者采用的拓扑结构差异较大。随着技术的发展,目前市场上 pci 设备越来越少,很多主板现在只提供 pcie 的接口了。
这些差异对在 uefi 下进行编程影响不大。uefi 系统已经屏蔽了这些差异,提供了一致的访问接口,下面详细介绍如何访问 pci/pcie 设备。
7.1.1 与 pci/pcie 设备通信的机制
pci 协议和 pcie 协议经过多年的发展,其内容已经非常庞大。本书主要论述的是如何在 uefi 下进行编程。站在软件工程师的角度,在访问 pci/pcie 设备时,实际上只要回答以下两个问题就可以了。
ø如何在系统中找到需要访问的设备?
ø找到设备后,如何访问设备内的寄存器或其他资源?
uefi 规范中,抽象了 pci 的系统架构,典型的桌面系统的 pci 架构如图 7-1 所示。
图 7-1 单 pci root bridge 的桌面系统
一般的桌面系统只有一个 pci host bus(pci 主机总线),用于完成 cpu 与 pci 设备之间的数据交换。pci root bridge(pci 根桥)一般也只有一个,它管理一个局部总线,下挂一棵 pci 总线树。我们所要访问的 pci 设备,就挂在这棵总线树上,它们属于同一总线空间,如图 7-2 所示。
从图 7-2 中可以看出,pci 总线树上包含 pci 总线、pci 桥和 pci 设备。系统通过三段编码的方式进行编码,即通过 bus number(总线号)、device number(设备号)和 function number(功能号)来编码,这种编码一般简称为 bdf 码。bdf 码在 bios 进行 pci 总线扫描和枚举过程中确定,可以用来作为查找 pci 设备的索引。
图 7-2    pci 总线树
找到 pci 设备后,如何确定此设备就是自己要找的设备呢?每个 pci 设备,除了主总线桥外,都会实现配置空间(主总线桥可以有选择地实现),而在配置空间中,包含了设备厂商用来标志自身的 vendor id(供应商 id)和 device id(设备 id)。通过比对 pci 设备的供应商 id 和设备 id,可以确定所找的设备是否为目标设备。
以 x86 平台为例,可通过 config_addr 寄存器(0xcf8)和 config_data 寄存器(0xcfc),以 bdf 码的形式访问 pci 设备,以得到设备的配置空间。图 7-3 所示为 pci设备的基本配置空间。
图 7-3    pci 设备的配置空间
pci 设备的基本配置空间由 64 字节组成,地址范围为 0x00~0x3f,主要用来识别设备、定义主机访问 pci 卡的方式。从图 7-3 中可以看出,最开始的两个寄存器就是 vendor id 和 device id 寄存器,这是用来标志设备自身的寄存器,由 pcisig 协会分配。比如intel 集成显卡 hd620,其 vendor id 为 0x8086,而 device id 为 0x5917。
在配置空间中,从地址 0x10 至 0x24,包含了 6 个 base address registe(r  基址寄存器)。这组寄存器被称为 bar,保存了 pci 设备使用的地址空间的基地址,也即该设备在 pci 总线域中的地址。每个 pci 设备最多可以有 6 个基址空间,但多数设备不会使用这么多,笔者以前常用的南京沁恒的 ch366 芯片,只使用了第一个 bar。
bar 可寻址 io 地址空间或者 memory 地址空间,其最低位是只读位,显示了可以访问哪种地址空间。值为 0 表示寄存器是 memory 地址译码,值为 1 表示寄存器是 io 地址译码,如图 7-4 所示。
图 7-4    基地址寄存器的位分配
那么如何访问 pci 设备内的寄存器和其他资源?答案是通过 bar 寄存器,加上内部寄存器相对于 bar 的偏移地址。芯片手册中,会提供关于内部资源的使用说明,以 ch366 的芯片为例,其内部寄存器说明如图 7-5 所示。
图 7-5 ch366 寄存器说明(节选自《ch366 中文手册》)
ch366 的第一个 bar 可以使用,它是 io 地址译码的,其他 bar 在芯片中是无效的。图 7-4 表明,第一个 bar 加上偏移地址,就可以成为芯片内部相应功能的寄存器。至于这些内部寄存器有什么作用,则需要详细了解芯片手册才能知道。
总结来说,访问 pci/pcie 设备的过程如下。
1)扫描整个系统空间,通过 bdf 获取 pci/pcie 设备的配置空间。同一总线域上(即存在一个主桥),pcie 一共支持 256 个总线、32 个设备、8 个功能,也就是说总线号最大值为 255、设备号最大值为 31、功能号最大为 7。
2)读取 pci/pcie 设备配置空间中的 vendor id 和 device id,确定是否为需要访问的设备。
3)找到 pci/pcie 设备后,获取其配置空间中的 bar,参照芯片手册,访问设备的内部寄存器和资源。
uefi 中提供了两个主要的模块来支持 pci 总线,一是 pci host bridge(pci 主桥)控制器驱动,另一个是 pci 总线驱动。这两个模块是和特定的平台硬件绑定的,在这种机制下,屏蔽了不同的 cpu 架构差异,为软件开发者提供了比较一致的 protocol 接口。下一节详细介绍访问 pci/pcie 设备的 protocol。
7.1.2 支持访问 pci/pcie 设备的 protocol
uefi 标准中提供了两类访问 pci/pcie 设备的 protocol—efi_pci_root_bridge_ io_protocol 和 efi_pci_io_protocol。前者为 pci 根桥提供了抽象的 io 功能,它由 pci host bus controller(pci 主总线驱动器)产生,一般由 pci/pcie 总线驱动用来枚举设备、获得 option rom、分配 pci 设备资源等;后者由 pci/pcie 总线驱动为 pci/pcie 设备产生,一般由 pci/pcie 设备驱动用来访问 pci/pcie 设备的 io 空间、memory 空间和配置空间。
这两种 protocol 的使用方法如下。
1.使用 efi_pci_root_bridge_io_protocol
efi_pci_root_bridge_io_protocol 中提供了基本的访问接口,包括访问 io 空间、memory 空间和配置空间的接口。该 protocol 主要由 pci/pcie 总线驱动使用,当然, uefi 应用也可以使用它来遍历 pci/pcie 设备。
该 protocol 中还提供了 dma 接口,以支持总线驱动访问系统内存。代码清单 7-1 给出了 efi_pci_root_bridge_io_protocol 的函数接口。
代码清单 7-1 efi_pci_root_bridge_io_protocol 函数接口
typedef struct _efi_pci_root_bridge_io_protocol { efi_handle parenthandle; //protocol的父句柄efi_pci_root_bridge_io_protocol_poll_io_mem pollmem; efi_pci_root_bridge_io_protocol_poll_io_mem pollio;efi_pci_root_bridge_io_protocol_access mem; //读写memory空间efi_pci_root_bridge_io_protocol_access io; //读写io空间efi_pci_root_bridge_io_protocol_access pci; //读写配置空间efi_pci_root_bridge_io_protocol_copy_mem copymem; efi_pci_root_bridge_io_protocol_map map; efi_pci_root_bridge_io_protocol_unmap unmap;efi_pci_root_bridge_io_protocol_allocate_buffer allocatebuffer; efi_pci_root_bridge_io_protocol_free_buffer freebuffer; efi_pci_root_bridge_io_protocol_flush flush; efi_pci_root_bridge_io_protocol_get_attributes getattributes; efi_pci_root_bridge_io_protocol_set_attributes setattributes; efi_pci_root_bridge_io_protocol_configuration configuration;uint32 segmentnumber;} efi_pci_root_bridge_io_protocol;  
从代码清单 7-1 中可以看出,此 protocol 提供了非常丰富的访问接口。由于篇幅所限, 无法将所有接口都介绍清楚,这里主要介绍如何读写 pci/pcie 设备的 3 种空间。
从 7.1.1 节的介绍中我们知道,pci/pcie 设备能访问的空间包括 memory 空间、io 空间和配置空间。对于这 3 种空间,每个 pci/pcie 设备必须实现配置空间,而 memory 空间和 io 空间的功能,则不一定实现。7.1.1 节介绍的 pcie 芯片 ch366 就只支持 io 空间的访问。
efi_pci_root_bridge_io_protocol 提供了访问 memory 空间的接口 mem、访问 io 空间的接口 io 和访问配置空间的接口 pci。这 3 个接口的参数类型都是一样的,均为efi_pci_root_bridge_io_protocol_access,详见代码清单 7-2。
代码清单 7-2 访问 io 空间、memory 空间和配置空间的接口
typedef struct {efi_pci_root_bridge_io_protocol_io_mem read; //读数据efi_pci_root_bridge_io_protocol_io_mem write; //写数据} efi_pci_root_bridge_io_protocol_access;typedef efi_status (efiapi *efi_pci_root_bridge_io_protocol_io_mem) (in efi_pci_root_bridge_io_protocol *this, //实例in efi_pci_root_bridge_io_protocol_width width, //读写宽度in uint64 address, //io空间/memory空间/配置空间的地址in uintn count, //读写的数据个数,单位为读写宽度widthin out void *buffer //对读操作,这是目的缓冲区;对写操作,这是要写的数据缓冲区);  
访问 3 种空间的接口都包含读数据和写数据两种操作,并且使用了同样的数据结构efi_pci_root_bridge_io_protocol_io_mem。在此结构中,width 是指读写宽度, 其值由枚举类型 efi_pci_root_bridge_io_protocol_width 给出,一般包括 8 位、16 位、32 位和 64 位几种。
需要注意的是,参数 address 在访问 io 空间、memory 空间和配置空间时,其含义是不同的。对配置空间而言,address 由 bdf 地址和 register 偏移决定,即总线号、设备号、功能号和 register 共同给出的寻址用索引。这是一个 64 位长的参数,一般使用宏 efi_ pci_address 来组合 bdf 和 register 偏移。在 edk2 中,这个宏定义于头文件 mdepkg includeprotocolpcirootbridgeio.h 中,其内容如下。
#define efi_pci_address(bus, dev, func, reg) (uint64) ( (((uintn) bus) << 24) | (((uintn) dev) << 16) | (((uintn) func) << 8) | (((uintn) (reg)) locatehandlebuffer(byprotocol, &gefipcirootbridgeioprotocolguid, null,&handlecount, &pcihandlebuffer);if (efi_error(status)) return status;print(lfind pci root bridge i/o protocol: %d,handlecount);//获取pcirootbridgeioprotocol实例for (handleindex = 0; handleindex handleprotocol( pcihandlebuffer[handleindex], &gefipcirootbridgeioprotocolguid, (void**)&gpcirootbridgeio);if (efi_error(status)) continue; elsereturn efi_success;}return status;}efi_status locatepciio(void){efi_status status;efi_handle *pcihandlebuffer = null;uintn handleindex = 0;uintn handlecount = 0;//获取pciioprotocol的所有句柄 status = gbs->locatehandlebuffer(byprotocol, &gefipciioprotocolguid, null,&handlecount, &pcihandlebuffer);if (efi_error(status)) return status; //unsupport gpciio_count = handlecount;print(lfind pci i/o protocol: %d,handlecount);//获取pciioprotocol实例,并存储在全局变量gpciioarray中for (handleindex = 0; handleindex handleprotocol( pcihandlebuffer[handleindex], &gefipciioprotocolguid, (void**)&(gpciioarray[handleindex]));}return status;}  
示例 7-1 中提供了两个函数—locatepcirootbridgeio() 和 locatepciio(),用来获取需要测试的两类 protocol 的实例。获取实例的方法在 3.5 节中已经介绍过了,本节的例程用了同样的方法。efi_pci_root_bridge_io_protocol 的实例,在大部分办公用的个人电脑中只存在一个,因此直接用全局指针变量 gpcirootbridgeio 存储;而 efi_pci_io_ protocol 的实例存在多个,一般有多少个 pci/pcie 设备,就存在多少个实例,因此使用全局指针数组 gpciioarray[256] 来存储这些实例。
为遍历全部的 pci/pcie 设备,可以使用 gpcirootbridgeio 和 bdf 码,循环查找挂载总线上的设备,代码如示例 7-2 所示。
【示例 7-2】使用 efi_pci_root_bridge_io_protocol 遍历 pci/pcie 设备。
efi_status listpcimessage1(void){efi_status status=efi_success; pci_type00 pci;uint16 i,j,k,count=0; for(k=0;k<=pci_max_bus;k++)for(i=0;i<=pci_max_device;i++) for(j=0;j<=pci_max_func;j++){//判断设备是否存在status = pcidevicepresent(gpcirootbridgeio,&pci, (uint8)k,(uint8)i,(uint8)j);if (status == efi_success) //找到了设备{++count;print(l%02d. bus-%02x dev-%02x func-%02x: , count,(uint8)k,(uint8)i,(uint8)j);print(lvendorid-%x deviceid-%x classcode-%x, pci.hdr.vendorid,pci.hdr.deviceid,pci.hdr.classcode[0]);print(l);}}return efi_success;}  
从代码中可以看出,函数使用了 3 个 for 循环调用函数 pcidevicepresent(),依次寻找pci/pcie 设备是否存在。如果存在,则取出已经读取到的配置空间的数据,将设备的一些信息打印出来。
使用 efi_pci_io_protocol 遍历设备则比较简单,因为之前所得到的此 protocol 的实例,就是为 pci/pcie 设备产生的,实际上相当于找到了设备,只需要将设备的信息打印出来即可。相应的代码见示例 7-3 所示。
【示例 7-3】使用 efi_pci_io_protocol 遍历 pci/pcie 设备。
efi_status listpcimessage2(void){uintn i,count=0;pci_type00 pci;for(i=0;ipci.read(gpciioarray[i],efipciwidthuint32,0,sizeof (pci_type00) / sizeof (uint32),&pci);++count;print(l%02d. vendorid-%x deviceid-%x classcode-%x, count,pci.hdr.vendorid,pci.hdr.deviceid,pci.hdr.classcode[0]);print(l);}return efi_success;}  
本节所准备的示例,主要是为了演示如何使用与 pci/pcie 相关的两个 protocol。代码本身还有许多不完善的地方,比如对多个总线域情况的处理、内存的释放、protocol 的关闭等,都没有考虑。本书的代码,包括本节的代码在内,建议读者只用来学习使用,如果想商用,则应该在代码中将所有情况考虑到。
可参照 2.1.3 节的方法,设置编译的环境变量,并使用如下命令编译程序:
c:uefiworkspaceedk2uild -p robinpkgrobinpkg.dsc -m robinpkgapplicationslistpcimsg listpcimsg.inf -a x64  
所编译的程序最好在实际的机器上测试运行。笔者使用 2.2.2 节搭建的 qemu 环境来运行编译好的 64 位 uefi 程序,程序运行的结果如图 7-6 所示。
图 7-6    测试 listpcimsg 程序 


盘点新能源汽车自燃事故:今年已有20例,多为三元锂电池
比特币现金和比特币之间有什么差距
工业4.0如何帮助钢铁产业数字化转型
光存储高科技企业紫晶存储发布2021年报
音箱面板/音评价术语
如何在UEFI环境下使用 UEFI规范提供的接口
恩智浦携手OPPO在VOOC闪充技术领域的技术拓展
稳压二极管选型指南
因AR/VR受影响的行业有哪些?
助推节省燃料的汽车启动 / 停止电子系统
AI技术会怎样影响我们未来的生活
异构多核可编程系统原理与应用
LED调光的方法以及对发射出光的色彩稳定性影响分析
揭露那些4G可能改变和不会改变的事
中国成全球工业机器人最大买家
采用 Linduino 平台加快面向凌力尔特 IC 的固件开发
5G商用和新基建浪潮将进一步推动智慧城市建设提速
iphone8什么时候上市:一张图看懂iPhone8全新设计到底有何改动
云计算需要更低延迟,新型态的电脑架构和安全技术将应运而生
单克隆抗体是否能转化应用于临床?