专栏文章目录传送门:返回专栏目录
文章目录
目录
1. WIFI 模块简单介绍
2. 设备驱动原理介绍
3. PCIE WIFI驱动实例分析
3.1 查看设备树
3.2 wifi 设备驱动代码分析
3.3 内核配置选项
4. WIFI驱动调试相关
根据前面对PCIe的讲解,对PCIe的整体都有了一定的认识,具体工作原理有了一定了解,这章将在Linux系统下以PCIe接口的WIFI模块使用能够正常跑起来,环境如下:
-
cpu: i.mx8mq
-
OS:Android 11
-
Kernel version:kernel 5.4
-
WIFI module:AW-CM276MA-PUR模组(WIFI芯片88W8997)
1. WIFI 模块简单介绍
在我们认识WIFI模块,WIFI模块的接口主要有SDIO,USB,PCIe,采用PCIe接口对于其他这些接口来说,就是速度的优势,适用于需要更高性能和带宽的应用。如图AW-CM276MA-PUR模组采用的PCIe接口,采用M.2 2230接口;
查看NXP官方提供的相关AW-CM276MA-PUR
无线模块需要在i.MX 8M Quad主机系统上加载内核驱动程序,并在88W8987/88W8997 SoC上运行固件。MLAN模块会在SDIO/PCIe总线驱动程序检测到模块的SDIO/PCIe接口时,将固件二进制文件下载到SD8987/PCIE8997适配器中。内核驱动程序(SD8987/PCIE8997)会在总线驱动程序和内核中的“cfg80211”子系统之间加载。NXP内核驱动程序包含一组控制和配置,通过以下接口之一与用户空间进行通信
• 输入/输出控制(IOCTL)
• 无线扩展(Wext)
• CFG80211
IOCTL为用户空间应用程序(如iwconfig和iwpriv)提供了一条通路,而cfg80211为不同的用户空间应用程序(如wpa_supplicant、hostapd和iw)提供了另一条通路。图6说明了Wi-Fi层接口。
2. 设备驱动原理介绍
这里项目中将涉及到PCIe的RC和EP两端,目前Soc中的RC端的驱动芯片厂商已经提供,EP端的WIFI模块中的驱动wifi模块也已经是由厂家提供,如果是对于原厂厂商制作具有PCIe功能的产品那就需要实现PCIe驱动代码。本章节重点讲诉EP端相关。
对于EP设备原理以及驱动实现的过程如图
根据上图,EP设备驱动主要包含4个步骤:
-
初始化注册pci设备,当一个pci设备,首先需要去注册一个strcut pci_driver的结构体
-
填充struct pci_driver,里面包含实现PCIe的Vendor ID和Device ID,probe 接口,有需要添加pcie 电源管理接口;
-
PCIe设备probe,设备里面做了很多工作,设备的初始化资源,使能设备,申请设备资源,启动总线控制权,设置DMA传输方式,设置Bar空间映射,申请DMA中断;
-
创建字符设备驱动
3. PCIE WIFI驱动实例分析
3.1 查看设备树
从设备树入手,查看i.MX8MQ端PCIe接口
vim ./arch/arm64/boot/dts/freescale/imx8mq.dtsi
pcie1: pcie@33c00000 {
compatible = "fsl,imx8mq-pcie";
reg = <0x33c00000 0x400000>,
<0x27f00000 0x80000>;
reg-names = "dbi", "config";
#address-cells = <3>;
#size-cells = <2>;
device_type = "pci";
ranges = <0x81000000 0 0x00000000 0x27f80000 0 0x00010000 /* downstream I/O 64KB */
0x82000000 0 0x20000000 0x20000000 0 0x07f00000>; /* non-prefetchable memory */
num-lanes = <1>;
num-viewport = <4>;
interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 80 IRQ_TYPE_LEVEL_HIGH>; /* eDMA */
interrupt-names = "msi", "dma";
#interrupt-cells = <1>;
interrupt-map-mask = <0 0 0 0x7>;
interrupt-map = <0 0 0 1 &gic GIC_SPI 77 IRQ_TYPE_LEVEL_HIGH>,
<0 0 0 2 &gic GIC_SPI 76 IRQ_TYPE_LEVEL_HIGH>,
<0 0 0 3 &gic GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>,
<0 0 0 4 &gic GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>;
linux,pci-domain = <1>;
fsl,max-link-speed = <2>;
power-domains = <&pgc_pcie>;
resets = <&src IMX8MQ_RESET_PCIEPHY2>,
<&src IMX8MQ_RESET_PCIE2_CTRL_APPS_EN>,
<&src IMX8MQ_RESET_PCIE2_CTRL_APPS_CLK_REQ>,
<&src IMX8MQ_RESET_PCIE2_CTRL_APPS_TURNOFF>;
reset-names = "pciephy", "apps", "clkreq", "turnoff";
assigned-clocks = <&clk IMX8MQ_CLK_PCIE2_CTRL>,
<&clk IMX8MQ_CLK_PCIE2_PHY>,
<&clk IMX8MQ_CLK_PCIE2_AUX>;
assigned-clock-parents = <&clk IMX8MQ_SYS2_PLL_250M>,
<&clk IMX8MQ_SYS2_PLL_100M>,
<&clk IMX8MQ_SYS1_PLL_80M>;
assigned-clock-rates = <250000000>, <100000000>,
<10000000>;
status = "disabled";
};
在i.MX8MQ 中含有两个pcie,这里查看pcie1,主要定义了这个PCIE控制器在i.MX8MQ处理器上的配置和属性。列举一些重要的属性:
reg = <0x33c00000 0x400000>, <0x27f00000 0x80000>; // 定义寄存器空间的基地址和大小,用于PCIe控制器的访问;
一共定义两个元组 <0x33c00000 0x400000>定义了Device Bus Interface 寄存器空间;另外一个 <0x33c00000 0x400000>指定了配置空间;
device_type = "pci"; // 指示设备类型为 PCI
compatible = "pci-host-generic"; // 或其他 PCIe 控制器的兼容性字符串
num-lanes = <1>; // 指定通道数
fsl,max-link-speed = <2>; // 定义最大链路速度为Gen2
• ranges = <0x81000000 0 0x00000000 0x27f80000 0 0x00010000 /* downstream I/O 64KB */
• 0x82000000 0 0x20000000 0x20000000 0 0x07f00000>; /* non-prefetchable memory */
rangs 表示PCIe设备的地址范围和映射范围;
interrupt-map-mask = <0 0 0 0x7>; //定义中断映射掩码
以上这个pcie1: pcie@33c00000就是定义的Soc i.MX8MQ其中的一个pcie。在对于wifi设备是做一个ED端,设备树是如何定义,具体查看文件
vim ./arch/arm64/boot/dts/freescale/imx8mq-evk.dts
&pcie1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_pcie1>;
disable-gpio = <&gpio5 10 GPIO_ACTIVE_LOW>;
reset-gpio = <&gpio5 12 GPIO_ACTIVE_LOW>;
clocks = <&clk IMX8MQ_CLK_PCIE2_ROOT>,
<&clk IMX8MQ_CLK_PCIE2_AUX>,
<&clk IMX8MQ_CLK_PCIE2_PHY>,
<&pcie1_refclk>;
clock-names = "pcie", "pcie_aux", "pcie_phy", "pcie_bus";
assigned-clocks = <&clk IMX8MQ_CLK_PCIE2_AUX>,
<&clk IMX8MQ_CLK_PCIE1_PHY>,
<&clk IMX8MQ_CLK_PCIE1_CTRL>;
assigned-clock-rates = <10000000>, <100000000>, <250000000>;
assigned-clock-parents = <&clk IMX8MQ_SYS2_PLL_50M>,
<&clk IMX8MQ_SYS2_PLL_100M>,
<&clk IMX8MQ_SYS2_PLL_250M>;
vph-supply = <&vgen5_reg>;
l1ss-disabled;
status = "disabled";
wifi_wake_host {
compatible = "nxp,wifi-wake-host";
interrupt-parent = <&gpio5>;
interrupts = <11 IRQ_TYPE_LEVEL_LOW>;
interrupt-names = "host-wake";
};
};
其实对于ED设备,并不需要去怎么定义设备树,这里只是定义了wifi的唤醒相关,当唤醒后将会通过PCIE接口与它通信。pcie1表示一个
Root Complex设备,定义了相关的引脚,时钟,电源,状态,WIFI唤醒等配置。
3.2 wifi 设备驱动代码分析
初始化PCI 设备结构体,
查看pci设备接口体的数据结构
vim ./include/linux/pci.h
struct pci_driver {
struct list_head node;
const char *name;
const struct pci_device_id *id_table; /* Must be non-NULL for probe to be called */
int (*probe)(struct pci_dev *dev, const struct pci_device_id *id); /* New device inserted */
void (*remove)(struct pci_dev *dev); /* Device removed (NULL if not a hot-plug capable driver) */
int (*suspend)(struct pci_dev *dev, pm_message_t state); /* Device suspended */
int (*resume)(struct pci_dev *dev); /* Device woken up */
void (*shutdown)(struct pci_dev *dev);
int (*sriov_configure)(struct pci_dev *dev, int num_vfs); /* On PF */
const struct pci_error_handlers *err_handler;
const struct attribute_group **groups;
struct device_driver driver;
struct pci_dynids dynids;
ANDROID_KABI_RESERVE(1);
ANDROID_KABI_RESERVE(2);
ANDROID_KABI_RESERVE(3);
ANDROID_KABI_RESERVE(4);
};
作为一个PCIe设备就需要创建一个pci_driver,重点函数在
const struct pci_device_id *id_table; /* Must be non-NULL for probe to be called */ //EP设备的ID相关
int (*probe)(struct pci_dev *dev, const struct pci_device_id *id); /* New device inserted */ //EP probe
void (*remove)(struct pci_dev *dev); /* Device removed (NULL if not a hot-plug capable driver) */ // EP设备remove
int (*suspend)(struct pci_dev *dev, pm_message_t state); /* Device suspended */
int (*resume)(struct pci_dev *dev); /* Device woken up */
具体查看PCIe wifi 设备驱动(注:这里的驱动程序放在Android路径下,不在内核路径下)
./vendor/nxp-opensource/nxp-mwifiex/mxm_wifiex/wlan_src/mlinux/moal_pcie.c
/* PCI Device Driver */
static struct pci_driver REFDATA wlan_pcie = {
.name = "wlan_pcie",
.id_table = wlan_ids,
.probe = woal_pcie_probe,
.remove = woal_pcie_remove,
#ifdef CONFIG_PM
/* Power Management Hooks */
.suspend = woal_pcie_suspend,
.resume = woal_pcie_resume,
#endif
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 18, 0)
.err_handler = woal_pcie_err_handler,
#endif
};
支持的wlan_ids,这里将定义了vendor_id, Device_id
static const struct pci_device_id wlan_ids[] = {
#ifdef PCIE8897
{
PCIE_VENDOR_ID_NXP,
PCIE_DEVICE_ID_NXP_88W8897P,
PCI_ANY_ID,
PCI_ANY_ID,
0,
0,
},
#endif
#ifdef PCIE8997
{
PCIE_VENDOR_ID_NXP,
PCIE_DEVICE_ID_NXP_88W8997P,
PCI_ANY_ID,
PCI_ANY_ID,
0,
0,
},
{
PCIE_VENDOR_ID_V2_NXP,
PCIE_DEVICE_ID_NXP_88W8997P,
PCI_ANY_ID,
PCI_ANY_ID,
0,
0,
},
#endif
#ifdef PCIE9097
{
PCIE_VENDOR_ID_V2_NXP,
PCIE_DEVICE_ID_NXP_88W9097,
PCI_ANY_ID,
PCI_ANY_ID,
0,
0,
},
#endif
#ifdef PCIE9098
{
PCIE_VENDOR_ID_V2_NXP,
PCIE_DEVICE_ID_NXP_88W9098P_FN0,
PCI_ANY_ID,
PCI_ANY_ID,
0,
0,
},
{
PCIE_VENDOR_ID_V2_NXP,
PCIE_DEVICE_ID_NXP_88W9098P_FN1,
PCI_ANY_ID,
PCI_ANY_ID,
0,
0,
},
{},
}
这些设备的pci_device_id包含了什么信息,主要包含当前EP设备的Vendosr id, Device id 这些信息都是设备厂商提供的,都是需要去组织里面申请的。
注册PCI设备到内核pci_register_driver
/**
* @brief This function registers the IF module in bus driver
*
* @return MLAN_STATUS_SUCCESS or MLAN_STATUS_FAILURE
*/
mlan_status woal_pcie_bus_register(void)
{
mlan_status ret = MLAN_STATUS_SUCCESS;
ENTER();
/* API registers the NXP PCIE driver */
if (pci_register_driver(&wlan_pcie)) {
PRINTM(MFATAL, "PCIE Driver Registration Failed \n");
ret = MLAN_STATUS_FAILURE;
}
LEAVE();
return ret;
}
当加载这个wifi时候,匹配上设备树后 ,查看probe主要做了什么;
/**
* @brief This function handles PCIE driver probe
*
* @param pdev A pointer to pci_dev structure
* @param id A pointer to pci_device_id structure
*
* @return error code
*/
static int woal_pcie_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
pcie_service_card *card = NULL;
t_u16 card_type = 0;
int ret = 0;
ENTER();
PRINTM(MINFO, "vendor=0x%4.04X device=0x%4.04X rev=%d\n", pdev->vendor,
pdev->device, pdev->revision);
/* Preinit PCIE device so allocate PCIE memory can be successful */
if (woal_pcie_preinit(pdev)) {
PRINTM(MFATAL, "MOAL PCIE preinit failed\n");
LEAVE();
return -EFAULT;
}
card = kzalloc(sizeof(pcie_service_card), GFP_KERNEL);
if (!card) {
PRINTM(MERROR, "%s: failed to alloc memory\n", __func__);
ret = -ENOMEM;
goto err;
}
card->dev = pdev;
card_type = woal_update_card_type(card);
if (!card_type) {
PRINTM(MERROR, "pcie probe: woal_update_card_type() failed\n");
ret = MLAN_STATUS_FAILURE;
goto err;
}
woal_pcie_init(card);
if (woal_add_card(card, &card->dev->dev, &pcie_ops, card_type) ==
NULL) {
woal_pcie_cleanup(card);
PRINTM(MERROR, "%s: failed\n", __func__);
ret = -EFAULT;
goto err;
}
#ifdef IMX_SUPPORT
woal_regist_oob_wakeup_irq(card->handle);
#endif /* IMX_SUPPORT */
LEAVE();
return ret;
err:
kfree(card);
if (pci_is_enabled(pdev))
pci_disable_device(pdev);
LEAVE();
return ret;
}
probe 中主要对设备进行一个初始化,最初开始调用woal_pcie_preinit进行使能PCI设备,设置一些DMA地址掩码相关
/**
* @brief This function pre-initializes the PCI-E host
* memory space, etc.
*
* @param handle A pointer to moal_handle structure
*
* @return MLAN_STATUS_SUCCESS or MLAN_STATUS_FAILURE
*/
static mlan_status woal_pcie_preinit(struct pci_dev *pdev)
{
int ret;
if (pdev->multifunction)
device_disable_async_suspend(&pdev->dev);
ret = pci_enable_device(pdev); //使能PCI设备
if (ret)
goto err_enable_dev;
pci_set_master(pdev); //设置成总线主模式DMA模式, EP可以发起memory 方式请求
PRINTM(MINFO, "Try set_consistent_dma_mask(32)\n");
ret = pci_set_dma_mask(pdev, DMA_BIT_MASK(32)); //设置设备的DMA地址掩码32位,决定了使用物理内存范围
if (ret) {
PRINTM(MERROR, "set_dma_mask(32) failed\n");
goto err_set_dma_mask;
}
ret = pci_set_consistent_dma_mask(pdev, DMA_BIT_MASK(32)); // 设置设备的一致性 DMA 地址掩码为 32 位
if (ret) {
PRINTM(MERROR, "set_consistent_dma_mask(64) failed\n");
goto err_set_dma_mask;
}
return MLAN_STATUS_SUCCESS;
err_set_dma_mask:
pci_disable_device(pdev);
err_enable_dev:
return MLAN_STATUS_FAILURE;
}
申请PCIe内存映射到系统内存空间,这里重点查看woal_pcie_init,截取
ret = pci_request_region(pdev, 0, DRV_NAME); //请求分配第一个区域的内存资源
if (ret) {
PRINTM(MERROR, "req_reg(0) error\n");
goto err_req_region0;
}
card->pci_mmap = pci_iomap(pdev, 0, 0); //将第一个区域进行内存映射 BAR, 将其映射到驱动程序的虚拟内存地址空间中
if (!card->pci_mmap) {
PRINTM(MERROR, "iomap(0) error\n");
goto err_iomap0;
}
ret = pci_request_region(pdev, 2, DRV_NAME); //请求第二个区域的内存资源
if (ret) {
PRINTM(MERROR, "req_reg(2) error\n");
goto err_req_region2;
}
card->pci_mmap1 = pci_iomap(pdev, 2, 0); //将第二个区域进行内存映射 BAR
if (!card->pci_mmap1) {
PRINTM(MERROR, "iomap(2) error\n");
goto err_iomap2;
}
PRINTM(MINFO,
"PCI memory map Virt0: %p PCI memory map Virt2: "
"%p\n",
card->pci_mmap, card->pci_mmap1);
return MLAN_STATUS_SUCCESS;
从代码看到请求了两个区域的资源,也就是前几个章节提到的Configuration Space和Configuration Space,第一个用于配置,第二个用于数据的通道传输;
关于PCIe设备模式,这里采用PCIE_INT_MODE_MSI 中断方式
#ifdef PCIE
/* Enable/disable Message Signaled Interrupt (MSI) */
int pcie_int_mode = PCIE_INT_MODE_MSI;
static int ring_size;
#endif /* PCIE */
3.3 内核配置选项
CONFIG_WLAN_VENDOR_NXP=y
CONFIG_MXMWIFIEX=m
CONFIG_PM=y
....
还有一些必要的并未举例;
4. WIFI驱动调试相关
按照上面分析,将WIFI驱动模块编译放入设备运行,成功可以看到以下打印信息:
打印信息看到关键信息:
Link up
看到这个,表示与设备已经建立了通信链路,可以进行链路的交换;剩下很多pci 0001:01:那些信息都是WIFI设备相关的一些配置信息打印。
启动成功后,通过命令lspci可以看到WIFI PCIe的信息:
01:00.0 Class 0200: 1b4b:2b42
:
-
01:00.0
:PCI设备的域号、总线号和设备号。这个地址表示总线1上的设备0。 -
Class 0200
:设备的类别码,表示这是一个以太网控制器设备。 -
1b4b:2b42
:设备的厂商ID和设备ID。
调试PCIe出现的问题:
-
设备不能Link up上:
-
对硬件电路的排查,速率的确定监视信号是否正常;
注意:对于Android 相关调试并未列出,后续整理。