操纵系统怎样与设备进行交互

[复制链接]
发表于 2025-6-23 13:58:32 | 显示全部楼层 |阅读模式
前言

简单介绍一下操纵系统怎样与设备进行交互的。
正文

操纵系统与硬件通信的根本方式:
端口i/o:
这一个呢,是把设备标志为一个一个设备标为数字,然后向设备发送信号,
通过in和out指令访问设备寄存器。
  1. 从端口0x60读取键盘输入
  2. in al, 0x60
复制代码
这种比较简单,大体就是发送信号吧。
这里阐明一下,cpu与设备之间的沟通呢,很多时候我们总是想着是比如输出什么样的下令啥的。
有各种各样的复杂下令啥的,其实本质而言,到了最后都是对寄存器的操纵,所以在硬件层面呢,就不用去想什么下令,
本质而言是对寄存器的io操纵。
这种好像ok的,简单方便,可以对设备的寄存器进行读写,实现了根本功能。
但是随着要和设备的性能直接沟通的越来越频繁,这里就需要高性能了。
就出现了内存映射了。

那么下面来看一下内存映射这块:

  • 硬件层面的实现机制
(1) 地址空间划分
统一编址:CPU的物理地址空间被划分为 普通内存区域 和 设备寄存器区域。
例如:假设CPU有32位地址空间(4GB),硬件筹划时可能约定:
0x00000000-0xDFFFFFFF:DRAM(内存)
0xFE000000-0xFEFFFFFF:设备A的寄存器
0xFF000000-0xFFFFFFFF:设备B的寄存器
(2) 硬件地址解码
地址解码器(Address Decoder):
每个设备(如网卡、GPU)内置一个地址范围匹配电路。当CPU访问某个地址时:
CPU发出物理地址(如0xFE200000)。
内存控制器首先检查该地址是否属于DRAM:
如果是 → 访问内存芯片。
如果否 → 将地址转发到系统总线(如PCIe、AXI)。
设备监听总线:
设备比较总线地址与自身寄存器映射范围。
若匹配(如0xFE200000在设备A的范围内)→ 设备响应操纵,否则忽略。
(3) 寄存器访问的硬件行为
写入操纵:
((volatile uint32_t)0xFE200000) = 0x1234; // CPU执行内存写入指令

  • CPU将0xFE200000解释为普通内存地址,发出写请求(数据0x1234)。
  • 设备A的地址解码器辨认该地址属于自己,将数据0x1234写入内部寄存器(如控制寄存器)。
  • 硬件关键点:设备寄存器本质上是一个与地址绑定的触发器(Flip-Flop),写入时会触发设备行为(如启动DMA、修改工作模式)。
设备将当前寄存器值(如状态寄存器)返回给CPU,而非从内存读取数据。
uint32_t value = ((volatile uint32_t)0xFE200004); // CPU读取设备寄存器
操纵系统与硬件的协作
(1) 设备发现与资源分配
PCIe设备示例:
系统启动时,BIOS/UEFI或操纵系统通过PCI配置空间枚举设备,获取设备的MMIO需求(如需要256MB地址空间)。
操纵系统分配一段未被占用的物理地址范围(如0xFE000000-0xFE0FFFFF)给该设备,并写入设备的BAR(Base Address Register)。
设备收到BAR值后,将其作为自身寄存器的基地址。
(2) 内存映射到内核空间
Linux内核示例:
  1. void *regs = ioremap(0xFE000000, 0x100000); // 将物理地址映射到内核虚拟地址
复制代码
ioremap会将物理地址0xFE000000映射到内核的虚拟地址空间(如0xffff0000),后续驱动通过虚拟地址访问设备寄存器。
这样的话,那么我们的驱动就可以操纵设备了。
(3) 设备驱动的工作
写入寄存器:
  1. writel(0x1234, regs + 0x10); // 向寄存器偏移0x10处写入0x1234
复制代码
writel会编译为内存写入指令(如mov [eax], ebx),但现实地址指向设备寄存器而非内存。
驱动器这样写入即可。
这样就能驱动硬件了。
那么我们会问,一个硬件应该会有很多的寄存器才对,那么是怎样映射到不同的驱动器的呢,或者说怎样进行下令转换的呢?

  • 设备寄存器的地址布局
设备的所有寄存器会被映射到一段一连的物理地址空间,每个寄存器占据一个或多个地址(通常为4字节对齐)。
示例:假设某设备有3个寄存器,映射到基地址0xFE200000:
寄存器功能        偏移量(Offset)        完整物理地址        访问方式
控制寄存器        0x00        0xFE200000        写入下令字
状态寄存器        0x04        0xFE200004        读取状态
数据缓冲区        0x08        0xFE200008        读写数据
CPU访问逻辑:
写入0xFE200000 → 操纵控制寄存器。
读取0xFE200004 → 获取状态寄存器值。
写入0xFE200008 → 向数据缓冲区写入数据。

  • 硬件怎样辨认寄存器?
    设备内部通过地址解码逻辑区分不同寄存器,具体实现如下:
(1) 设备地址解码器
设备内部有一个地址匹配电路,根据CPU访问的地址偏移量选择对应的寄存器。
示例(简化逻辑):
  1. // Verilog示例:设备内部的地址解码逻辑
  2. always @(posedge clk) begin
  3.   if (cpu_addr == base_addr + 0x00)
  4.     control_reg <= cpu_data;  // 写入控制寄存器
  5.   else if (cpu_addr == base_addr + 0x04)
  6.     status_reg_read <= 1;     // 读取状态寄存器
  7.   else if (cpu_addr == base_addr + 0x08)
  8.     data_buffer <= cpu_data;  // 写入数据缓冲区
  9. end
复制代码
}

  • 物理地址分配的核心逻辑
资源分配通过 __pci_assign_resource() 实现:
// drivers/pci/setup-res.c
static int __pci_assign_resource(struct pci_bus *bus, struct pci_dev *dev,
int resno, resource_size_t size,
resource_size_t align, resource_size_t min)
{
struct resource *res = dev->resource + resno;
  1. // 假设网卡的寄存器偏移量定义
  2. #define NET_CTRL_REG   0x0000  // 控制寄存器
  3. #define NET_STATUS_REG 0x0008  // 状态寄存器
  4. void *base_addr = ioremap(0xFE000000, 0x40000); // 映射256KB
  5. writel(0x1, base_addr + NET_CTRL_REG);          // 写入控制寄存器
  6. uint32_t status = readl(base_addr + NET_STATUS_REG); // 读取状态寄存器
复制代码
}
资源分配器(pci_bus_alloc_resource)
终极调用内核的通用资源管理接口:
  1.     // 判断是MMIO还是I/O空间
  2.     if (l & PCI_BASE_ADDRESS_SPACE_IO) {
  3.         /* I/O空间 */
  4.     } else {
  5.         /* MMIO空间 */
  6.         mask = PCI_BASE_ADDRESS_MEM_MASK;
  7.         res->flags |= IORESOURCE_MEM;
  8.     }
  9.     /* 保存BAR信息到pci_dev->resource[] */
  10. }
复制代码
这里分配物理地址是告诉cpu,这段范围作为设备内存映射,而不是去访问内存地址。
也就是说cpu知道了是内存映射后,不是去访问我们的内存条,而是丢给系统总线。
这样这块物理内存就被标记了。
标记了之后,我们的内核程序又无法直接访问物理地址。
那么需要我们再次申请一下映射,这样执行内核代码的时候就会访问到这块物理地址了。

  • 物理地址到虚拟地址的映射(ioremap)
分配物理地址后,驱动需要通过 ioremap 将其映射到内核虚拟地址空间:
  1. /* 获取BAR请求的大小和对齐 */
  2. size = resource_size(res);
  3. min = (res->flags & IORESOURCE_IO) ? PCIBIOS_MIN_IO : PCIBIOS_MIN_MEM;
  4. align = pci_resource_alignment(dev, res);
  5. /* 调用资源分配器分配物理地址 */
  6. ret = _pci_assign_resource(dev, resno, size, align, min);
  7. if (ret == 0) {
  8.     /* 将分配的物理地址写入BAR */
  9.     pci_update_resource(dev, resno);
  10. }
  11. return ret;
复制代码
也就是分为两部分,一部分是申请阶段,做了标记,另外一部分做的是映射,没有映射永远访问不到这块物理地址。


下一节,写到哪,到哪吧。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
回复

使用道具 举报

×
登录参与点评抽奖,加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表