PCI总线学习笔记:读写篇

前言

最近在写E1000网卡的驱动,这其中涉及到了PCI总线的相关内容。但是网上大部分关于PCI的文章都只局限在概念上的描述,并没有给出具体的例子来解释。这其实也是情理之中的,因为PCI总线规范就像是一个抽象的接口,其具体怎么实现是与具体的设备有关的,这也是学习硬件最让人头痛的地方:有时难以区分概念与实现的边界,例如,对于CPU是如何区分访存和MMIO这个问题(两者在CPU看来都是对一个物理地址进行访问),作为概念层的x86体系结构规范并没有明确规定这个该如何实现,而只是在手册里面提到了这样一个概念;而其具体实现方式在不同的CPU型号之间是不同的,例如在Intel Xeon系列CPU中就是通过一个叫做SAD(Source Address Decoder)的硬件来完成的。

Anyway,本文旨在记录一些我在学习中遇到的问题,以及这些问题的答案,希望能够给到读者一些帮助。

如果需要更加深入地理解PCI总线工作原理,建议配合《PCI Express体系结构导读》(王齐 著)使用。

PCI设备是如何读写的?

PCI是总线规范,其读写是通过总线事务来完成的,简单来说就是,按照一定的约定,向总线上写入事务类型,地址等参数,然后再使用数据线传输数据。(具体过程可以参考《PCI Express体系结构导读》)
(需要注意的是,这里的地址其实是PCI域的地址,和存储器域的地址并不等价,两者要通过HOST主桥做转换,但是由于在x86下,存储器域地址和PCI域地址在数值上是相等的,所以本文不再区分这个概念了,统一使用地址这个名词,这部分具体见《PCI Express体系结构导读》)

如何遍历PCI总线来发现存活设备?

实现思路:可以通过枚举所有可能的Bus Number, Device Number 和 Function Number来探测所有的存活设备。对于一组特定的Bus, Dev和Func,如果这个功能存在,那么在其配置空间中的Vendor ID和Device ID就是有效值,可以由此来判断。

如何访问配置空间?

访问配置空间的方式与具体的体系机构有关,例如在 MPC8548 处理器的 HOST 主桥中,与 PCI 设备配置空间相关的寄存器由CFG_ADDR、CFG_DATA 和 INT_ACK 寄存器组成。系统软件使用 CFG_ADDR 和 CFG_DATA 寄存器访问PCI 设备的配置空间,软件通过向CFG_ADDR寄存器中写入地址,然后访问CFG_DATA寄存器,当CFG_ADDR的EN位为1时,HOST 主桥将对这个寄存器的访问转换为 PCI 配置读写总线事务并发送到 PCI 总线上。而在x86体系结构下,CFG_ADDR 和 CFG_DATA是通过2个IO端口来实现的,CONFIG_ADDRESS地址是0xcf8,CONFIG_DATA地址是0xcfc,也就是说,可以通过IN和OUT指令对这两个端口进行读写来实现对配置空间的访问。
虽然访问CFG_ADDR的方式与具体的体系结构有关,但是CFG_ADDR的格式是由PCI Spec规定好了的,其具体含义如下图所示:
在这里插入图片描述

Bus Number,Device Number与设备被插在主板上哪个PCI插槽有关,其编号方式示意图如下(不完全严谨,但是这个不用细究,只需要知道这两个值可以唯一确定一个PCI插槽即可):

在这里插入图片描述

Function Number表示PCI设备上的功能号,一个PCI设备可以最多有8个功能(但是一般的PCI设备都是单功能,只是PCI规范提供了扩展的一种可能性)。
Register Number的含义见下图(配置空间也是PCI Spec规定了的内容,这里只展示了PCI设备的配置空间,PCI桥的配置空间略有差别):
在这里插入图片描述
例如,如果我想要访问某个设备某个功能的Revision ID,那么Register Number就设置为0x08,然后读取一个4字节的数据,取其中的第一个字节即可。

BAR寄存器如何工作的?

这也是初学者容易迷惑的地方,这一章节将尝试回答如下问题:

  • BAR寄存器保存的地址是什么地址?有什么用?
  • BAR寄存器中的值是谁负责分配的?

BAR寄存器的作用?

向BAR寄存器中写入了一个地址就相当于标记了这段地址是属于这个BAR的了,以后所有对这个地址的访存操作都会转发到这个设备,由这个设备进行操作。
例如,如果我有一个E1000网卡,我把这个网卡配置空间的BAR0设置为了0xabcde000,那么当我向0xabcde002的位置写入数据的时候,E1000网卡就会收到这个写数据的操作,并进行相应的动作。而具体访问这个地址会造成什么结果,这个是和具体的设备相关的,例如,对于E1000网卡,BAR0对应的是E1000相关寄存器,即如果BAR0设置为了0xabcde000,那么访问0xabcde000到0xabcdefff就等价于访问了E1000网卡的寄存器,如下图所示:
在这里插入图片描述
而具体每个地址对应到哪个寄存器,每个寄存器是什么作用,则需要继续查阅E1000的手册,下图是部分寄存器的偏移量以及名称:
在这里插入图片描述
(注:BAR寄存器有IO模式和MEM模式,MEM模式就是上面所述的情况,可以直接通过访存来实现,而IO方式则需要通过IN和OUT指令来访问IO端口来实现,这里不再赘述了)

BAR寄存器的大小?

接下来的问题是:这里我只设置了一个Base Address,我怎么知道这个区域的大小呢?例如,我把BAR0设置为了0xabcde000,那么为什么对地址0xbbcde000的访问不会转发到这个设备来呢?
这其实是通过一个规定来实现的,即如果BAR空间的大小为M,那么BAR寄存器中地址的低 l o g 2 M log_2M log2M位一定是0。例如,BAR1对应的空间大小是 2 12 2^{12} 212字节,那么BAR1的值一定是0xfffff000,0xabcde000之类的,不可以是0xabcde010,因为需要保证这个地址的二进制位的低12位是0。
而且这种规定还是由硬件来实现的,也就是说,如果这个空间大小是 2 12 2^{12} 212字节,那么即使我向这个BAR寄存器里面写0xfffffff,最终这个寄存器里的值只会是0xfffff000,硬件会自动把低12位给强制置零。事实上,软件也是通过这个小trick来获取到这个BAR空间的大小的。
(而且推测硬件也是通过这种方式来快速匹配总线事物的目标设备是不是自己,因为如果这个BAR空间大小是 2 12 2^{12} 212字节,那么只需要把地址线的高20位和BAR寄存器的高20位做比较即可得出结论)
(具体可参考这篇Stackoverflow Post)
(事实上,IO模式还是MEM模式,这也是由硬件定好的,软件是不可能通过写寄存器来更改的)

怎么就知道是访问这个设备了?

现在又有一个问题:我只是在E1000网卡的BAR0寄存器设置了一个值,然后我使用MOV $0xabcde002,%eax指令(假设虚拟地址0xabcde002对应的物理地址就是0xabcde002),CPU就会去找E1000网卡了。那么CPU是怎么知道这个信息的?
这涉及到总线的工作原理了,读0xabcde002这个指令不是单独发给E1000网卡的,而是广播给了总线上的所有设备,主设备会把访存地址发送到地址线上,然后每个PCI设备都拿地址线上的地址和自己的BAR寄存器匹配,如果匹配上了,就按照控制线上的指示进行操作,并通过数据线来传递数据。(具体可见这篇博客文章)

此时还有一个问题,CPU拿到MOV $0xabcde002,%eax指令的时候,只知道要去访问物理地址0xabcde002,那它为什么不去访问存储器的对应位置,而是把访存请求发到了E1000设备?
答案是:有相应的硬件设备来进行这种路由操作,不同型号的CPU对于这个功能的实现不尽相同,例如(参考资料【3】【4】),对于Intel Xeon系列的CPU,其内部有一个叫做SAD(Source Address Decoder)的硬件,这个硬件保存了对于MMIO区域的配置,负责把MMIO请求转发到PCI主桥中。
(注意,上文提到的0xabcde002指的都是物理地址,实际上在开启分页后,CPU处理的都是虚拟地址,虚拟地址需要通过MMU转换为物理地址)
更通用地来讲,对于x86架构,这部分工作应该是由北桥(North Bridge)来完成的,北桥应该负责把访问PCI设备的请求转发到PCI总线上。
在这里插入图片描述

MMIO究竟是怎么实现的?

有了上面的铺垫,还原MMIO的全过程就很简单了。
这里假设我把E1000网卡的BAR0设置为了0xabcde000,MEM类型;现在想要访问E1000网卡中offset为0的寄存器,且已知虚拟地址0xffabe000经过MMU变换成物理地址之后是0xabcde000,那么现在只需要一个movl $0xffabe000, %eax指令,就可以把想要的值存储到eax中。
上述整个过程具体分解如下:

  1. 0xffabe000这个地址通过MMU转换为物理地址0xabcde000
  2. CPU把读物理地址0xabcde000的请求发到北桥
  3. 北桥看出这是一个MMIO区域的地址,所以把请求发到PCI总线
  4. PCI总线上的E1000网卡匹配成功,把数据发送到总线的数据线上
  5. 数据逐层向上传递到CPU

逐级向下转发是如何实现的?

PCI桥也有自己的Base和Limit寄存器,可以记录这个PCI桥所管辖的地址范围,所以可以实现向下转发,具体见这篇文章。

BAR寄存器地址是谁分配的?

通过上面的描述,可以看出,BAR寄存器地址分配是一个难度比较高的任务,因为需要保证每个BAR寄存器的值之间一定不能有冲突,否则就会出现2个PCI设备同时响应一个总线事务的混乱局面,那么这个BAR寄存器值是谁分配的呢?
答案是BIOS等firmware在启动时分配的,而我们写操作系统时只需要读取firmware预分配好的值,然后直接利用就行,这个分配工作不需要操作系统来完成,具体见这篇Stackoverflow Post。

参考资料

【1】《PCI Express体系结构导读》(王齐 著)
【2】Intel E1000 Manual
【3】Physical Address Decoding in Intel Xeon v3/v4 CPUs: A Supplemental Datasheet
【4】Intel Xeon 7500 Datasheet

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/796863.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C语言之自定义类型联合和枚举

目录 前言 一:联合体(共用体)union 1.联合体类型的声明 2.联合体的特点 3.联合体大小的计算 4.联合体判断机器的大小端 二:枚举enum 1.概念 2.枚举的优点 3.枚举的使用 接下来的日子会顺顺利利,万事胜意…

C++ 类和对象(中篇)

类的6个默认成员函数 如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情 况下,都会自动生成下面6个默认成员函数。 构造函数: 定义:构造函数是一个特殊的成员…

【C语言】猜数字小游戏(并讲解随机数相关知识)

前言 一、游戏菜单 二、游戏逻辑 1.用户选择 2.开始游戏 2.1 生成1~100的随机数 总结 前言 本文讲解使用C语言写一个猜数字小游戏(1~100),涉及到的语法为:循环、分支、随机数、函数 一、游戏菜单 一个游戏的最开始,往往是一个菜单&…

Jetpack Bluetooth——更优雅地使用蓝牙

Jetpack Bluetooth——更优雅地使用蓝牙 蓝牙是安卓开发中非常常用的操作,但安卓经过这么多年的迭代,蓝牙的相关接口都经过了很多修改需要适配,还有的接口需要实现一堆函数。。。整套操作虽说不算复杂,但难免感觉不太舒服。 之前…

专题【双指针】【学习题】刷题日记

题目列表 11. 盛最多水的容器 42. 接雨水 15. 三数之和 16. 最接近的三数之和 18. 四数之和 26. 删除有序数组中的重复项 27. 移除元素 75. 颜色分类 167. 两数之和 II - 输入有序数组 2024.04.06 11. 盛最多水的容器 题目 给定一个长度为 n 的整数数组 height 。有 n 条垂…

PHP实现网站微信扫码关注公众号后自动注册登陆实现方法及代码【关注收藏】

在网站注册登陆这环节,增加微信扫码注册登陆,普通的方法需要开通微信开发者平台,生成二维码扫码后才能获取用户的uinonid或openid,实现注册登陆,但这样比较麻烦还要企业认证交费开发者平台,而且没有和公众号…

【SQL】1890. 2020年最后一次登录(简单写法;窗口函数写法)

前述 sql 中 between 的边界问题 ---- between 边界:闭区间,not between 边界:开区间 在 sql 中, between 边界:闭区间not between 边界:开区间 题目描述 leetcode题目:1890. 2020年最后一…

【leetcode面试经典150题】16.接雨水(C++)

【leetcode面试经典150题】专栏系列将为准备暑期实习生以及秋招的同学们提高在面试时的经典面试算法题的思路和想法。本专栏将以一题多解和精简算法思路为主,题解使用C语言。(若有使用其他语言的同学也可了解题解思路,本质上语法内容一致&…

aardio教程五) 写Python风格的aardio代码(字符串篇)

前言 熟悉一个新的语言最麻烦的就是需要了解一些库的使用,特别是基础库的使用。 所以我想给aardio封装一个Python风格的库,Python里的基础库是什么方法名,aardio里也封装同样的方法名。 这样就不需要单独去了解aardio里一些方法的使用细节…

Lanelets_ 高效的自动驾驶地图表达方式

Lanelets: 高效的自动驾驶地图表达方式 附赠自动驾驶学习资料和量产经验:链接 LaneLets是自动驾驶领域高精度地图的一种高效表达方式,它以彼此相互连接的LaneLets来描述自动驾驶可行驶区域,不仅可以表达车道几何,也可以完整表述车…

AIGC实战——ProGAN(Progressive Growing Generative Adversarial Network)

AIGC实战——ProGAN 0. 前言1. ProGAN2. 渐进式训练3. 其他技术3.1 小批标准差3.2 均等学习率3.3 逐像素归一化 4. 图像生成小结系列链接 0. 前言 我们已经学习了使用生成对抗网络 (Generative Adversarial Network, GAN) 解决各种图像生成任务。GAN 的模型架构和训练过程具有…

真实的招生办对话邮件及美国高校官网更新的反 AI 政策

这两年 ChatGPT 的热度水涨船高,其编写功能强大,且具备强大的信息整合效果,所以呈现的内容在一定程度上具备可读性。 那么,美国留学文书可以用 ChatGPT 写吗?使用是否有风险?外网博主 Kushi Uppu 在这个申…

基于卷积神经网络的天气识别系统(pytorch框架)【python源码+UI界面+前端界面+功能源码详解】

功能演示: 天气识别系统,vgg16,mobilenet卷积神经网络(pytorch框架)_哔哩哔哩_bilibili (一)简介 基于卷积神经网络的天气识别系统是在pytorch框架下实现的,系统中有两个模型可选…

vue+elementUI实现表格组件的封装

效果图&#xff1a; 在父组件使用表格组件 <table-listref"table":stripe"true":loading"loading":set-table-h"slotProps.setMainCardBodyH":table-data"tableData":columns"columns.tableList || []":ra…

基于Springboot的Java学习平台

采用技术 基于Springbootjava学习平台的设计与实现~ 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBootMyBatis 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 页面展示效果 系统功能模块 后台管理 用户注册 课程信息 作业信息 资料信息…

电工技术学习笔记——正弦交流电路

一、正弦交流电路 1. 正弦量的向量表示法 向量表示方法&#xff1a;正弦交流电路中&#xff0c;相量表示法是一种常用的方法&#xff0c;用于描述电压、电流及其相位关系。相量表示法将正弦交流信号表示为复数&#xff0c;通过复数的运算来描述电路中各种参数的相互关系 …

java中的正则表达式和异常

正则表达式&#xff1a; 作用一&#xff1a;用来校验数据格式是否合法 作用二&#xff1a;在文本中查找满足要求的内容 不用正则表达式&#xff1a;检验QQ号是否合法&#xff0c;要求全部是数字&#xff0c;长度在6-20&#xff0c;不能以0开头 public class test {public stat…

【Linux实践室】Linux高级用户管理实战指南:创建与删除用户组操作详解

&#x1f308;个人主页&#xff1a;聆风吟_ &#x1f525;系列专栏&#xff1a;Linux实践室、网络奇遇记 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 一. ⛳️任务描述二. ⛳️相关知识2.1 &#x1f514;Linux创建用户组命令2.1.1 知识点讲解2.1.2…

亲手开发全国海域潮汐表查询微信小程序详情教程及代码

最近在做一个全国海域潮汐表查询&#xff0c;可以为赶海钓鱼爱好者提供涨潮退潮时间表及潮高信息。 下面教大家怎么做一个这样的小程序。 主要功能&#xff0c;根据IP定位地理位置&#xff0c;自动查询出省份或城市的港口&#xff0c;进入后预测7天内港口潮汐表查询。 步骤&…

全坚固笔记本丨工业笔记本丨三防笔记本相较于普通笔记本有哪些优势?

三防笔记本和普通笔记本在设计和性能方面存在显著差异&#xff0c;三防笔记本相较于普通笔记本具备以下优势&#xff1a; 三防笔记本通常采用耐磨、耐摔的材料&#xff0c;并具有坚固的外壳设计&#xff0c;能够承受恶劣环境和意外碰撞&#xff0c;有效保护内部组件不受损坏。相…