Bootstrap

linux-5.10.110内核源码分析 - bcm2711 pcie MSI/MSI-x中断

1、查找保存MSI/MSI-x Capability地址

1.1、获取Capabilities Pointer指针的地址(__pci_bus_find_cap_start)

        根据header type获取Capabilities Pointer在配置空间的偏移,header type 1的Capabilities Pointer如上图所示为13(13*4):

1.2、查找MSI-X Capability(pci_find_capability)

        PCI Express Capability List Register如下,有个8位Capability ID,针对MSI-X Capability,该ID为0x11,还有一个Next Capability Pointer,指向下一个Capability在配置空间的地址,所有Capability通过Next Capability Pointer链接成一个单向链表,表头通过前面的Capabilities Pointer获取:

        MSI-X Capability结构:

        首先调用__pci_bus_find_cap_start,获取Capabilities Pointer在配置空间的地址:

        然后调用__pci_find_next_cap查找Capability,直到找到MSI-X Capability或者超过TTL为止:

        根据Capabilities Pointer、Next Capability Pointer遍历Capability链表,查找对应的Capability:

static int __pci_find_next_cap_ttl(struct pci_bus *bus, unsigned int devfn,
                                   u8 pos, int cap, int *ttl)
{
        u8 id;
        u16 ent;

        pci_bus_read_config_byte(bus, devfn, pos, &pos); // 读取Capabilities Pointer的值,也就是第一个Capability在配置空间的地址

        while ((*ttl)--) { // 循环查找Capability,直到*ttl为0,超过允许查找到最大次数
                if (pos < 0x40)
                        break;
                pos &= ~3; // 参考《PCI Local Bus Specification, Revision 3.0.pdf》P224“Capabilities Pointer”,低两位需要清零
                pci_bus_read_config_word(bus, devfn, pos, &ent); // 读取Capability的Capability ID及Next Capability Pointer

                id = ent & 0xff;
                if (id == 0xff) // 判断Next Capability Pointer
                        break;
                if (id == cap) // 如果id是要查找的Capability ID那么返回Capability地址
                        return pos; // 返回Capability地址
                pos = (ent >> 8); // pos是16位,前面读取的Capability的Capability ID及Next Capability Pointer,高8位是Next Capability Pointer,for循环继续读取下个Capability
        }
        return 0;
}

        函数调用栈如下:

1.3、保存MSI-X Capability地址(pci_msi_setup_pci_dev)

        将查找到的MSI-X Capability地址保存到pci_dev->msix_cap。

2、MSI-X Capability初始化(msix_capability_init映射MSI‐X Table)

2.1、MSI-X Capability及MSI‐X Table Entries

        Table Size、MSI-X Table offset、Table BIR如下,其中Table BIR表示MSI-X table在哪个BAR,MSI-X Table offset表示MSI-X table在BAR的偏移地址,Table Size表示MSI-X Table的大小:

                MSI‐X Table Entries如下:

 2.2、获取MSI‐X Table Size

        读取Table Size的代码如下,比较简单,就是读取配置空间对应的值进行计算:

2.3、映射MSI‐X Table(msix_map_region)

        映射MSI‐X Table的代码也比较简单,就是通过MSI-X Capability获取MSI‐X Table所在的BAR及在BAR的偏移地址,然后获取该BAR的cpu域地址,加上MSI‐X Table偏移就是MSI‐X Table的cpu域地址,映射完之后返回映射后的虚拟地址:

static void __iomem *msix_map_region(struct pci_dev *dev, unsigned nr_entries)
{
	resource_size_t phys_addr;
	u32 table_offset;
	unsigned long flags;
	u8 bir;

	pci_read_config_dword(dev, dev->msix_cap + PCI_MSIX_TABLE,
			      &table_offset); // 读取MSI-X Table Offset及Table BIR
	bir = (u8)(table_offset & PCI_MSIX_TABLE_BIR); // 获取BAR索引Table BIR
	flags = pci_resource_flags(dev, bir);
	if (!flags || (flags & IORESOURCE_UNSET))
		return NULL;

	table_offset &= PCI_MSIX_TABLE_OFFSET; // 获取MSI-X Table Offset
	phys_addr = pci_resource_start(dev, bir) + table_offset; // pci_resource_start(dev, bir)获取BAR的cpu域地址(具体可以参考前面的《linux-5.10.110内核源码分析 - bcm2711 pcie BAR地址分配》),加table_offset就是MSI-X Table的cpu域地址

	return ioremap(phys_addr, nr_entries * PCI_MSIX_ENTRY_SIZE); // 映射整个MSI-X Table,返回虚拟地址,通过该虚拟地址就可以像访问普通内存一样访问整个MSI-X Table
}

2.4、初始化MSI‐X Table Entry(msix_setup_entries)

        msix_setup_entries主要就是为每个MSI‐X Table Entry分配一个msi_desc,msi_desc记录MSI‐X Table Entry的索引、MSI‐X Table地址、vector Mask Bit,最终msi_desc链接到了设备的msi_list链表里面:

static int msix_setup_entries(struct pci_dev *dev, void __iomem *base,
			      struct msix_entry *entries, int nvec,
			      struct irq_affinity *affd)
{
	struct irq_affinity_desc *curmsk, *masks = NULL;
	struct msi_desc *entry;
	void __iomem *addr;
	int ret, i;
	int vec_count = pci_msix_vec_count(dev); // 获取MSI‐X Table Size

	if (affd)
		masks = irq_create_affinity_masks(nvec, affd);

	for (i = 0, curmsk = masks; i < nvec; i++) {
		entry = alloc_msi_entry(&dev->dev, 1, curmsk); // 分配一个msi_desc,用于描述一个MSI‐X Table Entry
		if (!entry) {
			if (!i)
				iounmap(base);
			else
				free_msi_irqs(dev);
			/* No enough memory. Don't try again */
			ret = -ENOMEM;
			goto out;
		}

		entry->msi_attrib.is_msix	= 1;
		entry->msi_attrib.is_64		= 1;

		if (entries)
			entry->msi_attrib.entry_nr = entries[i].entry;
		else
			entry->msi_attrib.entry_nr = i; // MSI‐X Table Entry索引

		entry->msi_attrib.is_virtual =
			entry->msi_attrib.entry_nr >= vec_count;

		entry->msi_attrib.default_irq	= dev->irq;
		entry->mask_base		= base; // MSI‐X Table的地址,不是当前MSI‐X Table Entry的地址,前面已经保存了MSI‐X Table Entry的索引,通过索引及MSI‐X Table的地址可以计算得到当前MSI‐X Table Entry的地址

		addr = pci_msix_desc_addr(entry); // 获取当前MSI‐X Table Entry的地址(已经设置了entry的MSI‐X Table的地址及索引,根据这两个值计算得到当前MSI‐X Table Entry的地址)
		if (addr)
			entry->masked = readl(addr + PCI_MSIX_ENTRY_VECTOR_CTRL); // 读MSI‐X Table Entry的Vector Control,获取vector Mask Bit

		list_add_tail(&entry->list, dev_to_msi_list(&dev->dev)); // 添加MSI‐X Table Entry到设备的msi_list链表里面,后续需要对设备的某个MSI‐X Table Entry操作的时候,通过索引在链表里面查找对应的MSI‐X Table Entry即可
		if (masks)
			curmsk++;
	}
	ret = 0;
out:
	kfree(masks);
	return ret;
}

3、MSI/MSI-x中断(MSI-X Table Entry分配irq_desc及硬件中断号)

3.1、为MSI-X Table Entry分配虚拟中断号(irq_domain_alloc_descs)

        __msi_domain_alloc_irqs循环为每个MSI-X Table Entry分配虚拟中断号,一个virq对应一个irq_desc,用于描述一个irq中断:

        调用栈:

3.2、为MSI-X Table Entry分配物理中断号(brcm_irq_domain_alloc)

        调用brcm_irq_domain_alloc为MSI-X Table Entry的virq分配一个中断号,代码中的domain->host_data为bcm2711 msi控制指针,最终保存到irq_desc->irq_data->chip_data,里面包含msi消息地址:

        调用栈:

        将硬件中断号保存到irq_desc->irq_data->hwirq里面:

        调用栈:

4、MSI/MSI-x中断(__msi_domain_alloc_irqs写MSI-X Table Entry)

4.1、激活irq(irq_domain_activate_irq)

        循环遍历pci设备的msi_list,对每个MSI-X Table Entry调用irq_domain_activate_irq激活中断,也就是把每个MSI-X Table Entry的中断信息写入到MSI-X Table Entry:

4.2、获取MSI/MSI-x消息地址(brcm_msi_compose_msi_msg)

        brcm_msi_compose_msi_msg通过irq_data获取brcm_msi,将brcm_msi消息地址保存到msi_msg里面,然后将硬件中断号也保存到msi_msg,硬件中断号也就是MSI/MSI-x的消息:

        调用栈:

4.3、写MSI/MSI-x消息(__pci_write_msi_msg)

        写MSI-X Table Entry的过程比较简单,前面初始化的时候为每个MSI-X Table Entry分配msi_desc的时候,在msi_desc里面保存了MSI-X Table的地址以及MSI-X Table Entry的索引,通过这两个即可获取到MSI-X Table Entry的地址,然后对相应的地址写数据即可。

        

void __pci_write_msi_msg(struct msi_desc *entry, struct msi_msg *msg)
{
	struct pci_dev *dev = msi_desc_to_pci_dev(entry);

	if (dev->current_state != PCI_D0 || pci_dev_is_disconnected(dev)) {
		/* Don't touch the hardware now */
	} else if (entry->msi_attrib.is_msix) {
		void __iomem *base = pci_msix_desc_addr(entry); // 获取MSI-X Table Entry的地址(通过MSI-X Table地址与索引计算得到)
		bool unmasked = !(entry->masked & PCI_MSIX_ENTRY_CTRL_MASKBIT);

		if (!base)
			goto skip;

		/*
		 * The specification mandates that the entry is masked
		 * when the message is modified:
		 *
		 * "If software changes the Address or Data value of an
		 * entry while the entry is unmasked, the result is
		 * undefined."
		 */
		if (unmasked)
			__pci_msix_desc_mask_irq(entry, PCI_MSIX_ENTRY_CTRL_MASKBIT);

		writel(msg->address_lo, base + PCI_MSIX_ENTRY_LOWER_ADDR); // 将消息地址到低地址部分写入MSI-X Table Entry
		writel(msg->address_hi, base + PCI_MSIX_ENTRY_UPPER_ADDR); // 将消息地址到高地址部分写入MSI-X Table Entry
		writel(msg->data, base + PCI_MSIX_ENTRY_DATA); // 将消息值写入MSI-X Table Entry

		if (unmasked)
			__pci_msix_desc_mask_irq(entry, 0);

		/* Ensure that the writes are visible in the device */
		readl(base + PCI_MSIX_ENTRY_DATA);
	} else {
		int pos = dev->msi_cap;
		u16 msgctl;

		pci_read_config_word(dev, pos + PCI_MSI_FLAGS, &msgctl);
		msgctl &= ~PCI_MSI_FLAGS_QSIZE;
		msgctl |= entry->msi_attrib.multiple << 4;
		pci_write_config_word(dev, pos + PCI_MSI_FLAGS, msgctl);

		pci_write_config_dword(dev, pos + PCI_MSI_ADDRESS_LO,
				       msg->address_lo);
		if (entry->msi_attrib.is_64) {
			pci_write_config_dword(dev, pos + PCI_MSI_ADDRESS_HI,
					       msg->address_hi);
			pci_write_config_word(dev, pos + PCI_MSI_DATA_64,
					      msg->data);
		} else {
			pci_write_config_word(dev, pos + PCI_MSI_DATA_32,
					      msg->data);
		}
		/* Ensure that the writes are visible in the device */
		pci_read_config_word(dev, pos + PCI_MSI_FLAGS, &msgctl);
	}

skip:
	entry->msg = *msg;

	if (entry->write_msi_msg)
		entry->write_msi_msg(entry, entry->write_msi_msg_data);

}

5、MSI/MSI-x中断使能(pci_msi_unmask_irq)

5.1、使能MSI控制器中断(brcm_pcie_enable_msi)

        pcie设备的消息发送到MSI控制器,MSI控制再发送中断到cpu,需要使能MSI控制中断:

        调用栈:

5.2、使能MSI/MSI-x中断(__pci_msix_desc_mask_irq)

        前面已经介绍在申请中断号的时候,MSI-X Table Entry的msi_desc已经保存到irq_data里面,使能中断的时候,通过中断号调用irq_to_desc即可获取到irq_desc,再获取irq_data,然后可以获取到MSI-X Table Entry的msi_desc。

        写vector Mask Bit:

u32 __pci_msix_desc_mask_irq(struct msi_desc *desc, u32 flag)
{
	u32 mask_bits = desc->masked; // 初始化的是已经把MSI-X Table Entryvector Mask Bit所在的整个word读取上来,vector Mask Bit仅占最低位
	void __iomem *desc_addr;

	if (pci_msi_ignore_mask)
		return 0;

	desc_addr = pci_msix_desc_addr(desc); // 获取MSI-X Table Entry的地址
	if (!desc_addr)
		return 0;

	mask_bits &= ~PCI_MSIX_ENTRY_CTRL_MASKBIT; // 清除mask_bits
	if (flag & PCI_MSIX_ENTRY_CTRL_MASKBIT) // 禁止中断
		mask_bits |= PCI_MSIX_ENTRY_CTRL_MASKBIT; // vector Mask Bit写1

	writel(mask_bits, desc_addr + PCI_MSIX_ENTRY_VECTOR_CTRL); // 写MSI-X Table Entry的vector Mask Bit

	return mask_bits;
}

        调用栈:

6、MSI/MSI-x中断服务程序(brcm_pcie_msi_isr)

6.1、brcm_pcie_msi_isr中断调用栈

        pcie设备发送消息的bcm2711的msi控制器,msi控制器发送中断到cpu,内核先调用msi中断处理函数brcm_pcie_msi_isr,如下图所示中断号180是msi控制器的硬件中断号:

6.2、获取MSI/MSI-x中断

        通过读取bcm2711 msi中断状态寄存器,获取有哪些中断发生:

6.3、处理MSI/MSI-x中断

        MSI_INT_STATUS中断寄存器的每个bit表示一个中断,从中断号初始化及中断处理代码看brcm_msi->used对应bit位为1,表示该中断号已经分配,中断号即为该bit位在brcm_msi->used对应的索引,MSI_INT_STATUS中断号同样是MSI_INT_STATUS对应bit位在MSI_INT_STATUS里面的索引,也就是pcie设备向msi控制器发送消息N,那么MSI_INT_STATUS的第N位会被设置为1,—— 没有具体芯片资料,从代码推理如此:

        MSI Capability与MSI-X Capability大部分代码都是相同的,具体可以参考PCIe协议手册。

;