Post

Aic8800

从0开始的Linux驱动学习:以AIC8800驱动为例

rwnx_main.c

1
2
3
4
5
6
7
8
9
10
module_init(rwnx_mod_init);
module_exit(rwnx_mod_exit);


MODULE_FIRMWARE(RWNX_CONFIG_FW_NAME);

MODULE_DESCRIPTION(RW_DRV_DESCRIPTION);
MODULE_VERSION(RWNX_VERS_MOD);
MODULE_AUTHOR(RW_DRV_COPYRIGHT " " RW_DRV_AUTHOR);
MODULE_LICENSE("GPL");

C 语言程序的入口是 main() 函数,但在 Linux 内核驱动里,没有 main 函数。内核怎么知道哪段代码是入口? 就是靠 module_initmodule_exit。一般来说,一个驱动只有一个 module_initmodule_exit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static int __init rwnx_mod_init(void)
{

    RWNX_DBG(RWNX_FN_ENTRY_STR);
    rwnx_print_version();
	AICWFDBG(LOGINFO, "RELEASE DATE:%s \r\n", RELEASE_DATE);
	rwnx_init_cmd_array();

	sema_init(&aicwf_deinit_sem, 1);
	atomic_set(&aicwf_deinit_atomic, 1);

	init_completion(&hostif_register_done);

	aicsmac_driver_register();

#ifdef AICWF_SDIO_SUPPORT
	if ((wait_for_completion_timeout(&hostif_register_done, msecs_to_jiffies(REGISTRATION_TIMEOUT)) == 0)) {
		AICWFDBG(LOGERROR, "register_driver timeout or error\n");
        aicwf_sdio_exit();
	return -ENODEV;
}

#endif /* AICWF_SDIO_SUPPORT */
#ifdef AICWF_USB_SUPPORT
       //aicwf_usb_exit();
#endif /*AICWF_USB_SUPPORT */


#ifdef AICWF_PCIE_SUPPORT
    return rwnx_platform_register_drv();
#else
    return 0;
#endif
}

__init是一个宏,用来标识这个函数是linux驱动启动函数。爱科微在写这套驱动时采用了高度抽象的设计。aic8800 这颗芯片在出厂时,可能有好几种硬件封装形态:

  • 焊接在低成本开发板上(走 SDIO 总线)
  • 做成外插的无线网卡、接收器(走 USB 总线)
  • 做成高端的 M.2 网卡插在电脑上(走 PCIe 总线)

原厂工程师不想为这三种接口写三套驱动,于是把核心的 Wi-Fi 协议处理(rwnx层)抽离出来,而把总线接口做成可配置的。在编译时,通过 Makefile 传入不同的宏,就能让驱动自动变为 SDIO、USB 或 PCIe 驱动。

当驱动刚加载时,多核 CPU 可能会产生混乱,为了确保安全退出初始化一个互斥信号量。在Linux内核中,信号量(semaphore)是一种用于进程或线程间同步的机制。信号量可以是二值的,即只有两个值,通常用于实现互斥锁(mutex),也可以有多个值,用于控制对共享资源的并发访问。信号量的一个关键特性是,如果一个任务尝试获取一个已经被占用的信号量,它会进入睡眠状态,直到该信号量被释放。例如:

1
2
3
#include <linux/semaphore.h>
struct semaphore sem;
sema_init(&sem, 1); // 初始化信号量,计数值设置为1

这里的sema_init(&aicwf_deinit_sem, 1);,是为了如果将来用户疯狂输入 rmmod 卸载驱动,或者系统关机触发卸载,能确保驱动卸载、释放内存在同一时刻只能有一个线程在干,不允许多个线程同时去释放内存。

在多核 CPU 里,如果两个核心同时去改一个普通的全局变量 int a,会导致数据错乱。而内核提供了 atomic_t(原子类型),它利用了 CPU 硬件层面的锁总线指令,确保对这个变量的读写是“不可分割”的绝对安全。atomic_set函数用于将原子类型变量的值设置为指定的整数值。原子操作需要硬件支持,因此与体系结构相关,通常用于实现资源计数。

在 Linux 内核中,completion 是一种非常轻量级的线程同步机制。简单来说,它的核心作用就是:让一个线程停下来死等,直到另一个线程通知某件事已经做完了,才继续往下走。init_completion(&hostif_register_done);就是在做等待初始化。

1
2
3
4
5
6
7
8
9
10
11
12
static void aicsmac_driver_register(void)
{
#ifdef AICWF_SDIO_SUPPORT
    aicwf_sdio_register();
#endif
#ifdef AICWF_USB_SUPPORT
    aicwf_usb_register();
#endif
#ifdef AICWF_PCIE_SUPPORT
    aicwf_pcie_register();
#endif
}

由于我前面一直在看基于AIC8800的USB无线网卡驱动部分,所以这里直接看aicwf_usb_register()

1
2
//aicwf_usb.h
extern void aicwf_usb_register(void);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static struct usb_driver aicwf_usbdrvr = {
    .name = KBUILD_MODNAME,
    .probe = aicwf_usb_probe,
    .disconnect = aicwf_usb_disconnect,
    .id_table = aicwf_usb_id_table,
    .suspend = aicwf_usb_suspend,
    .resume = aicwf_usb_resume,
    .reset_resume = aicwf_usb_reset_resume,
#ifdef ANDROID_PLATFORM
    .supports_autosuspend = 1,
#else
    .supports_autosuspend = 0,
#endif
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 5, 0)
    .disable_hub_initiated_lpm = 1,
#endif
};

void aicwf_usb_register(void)
{
    if (usb_register(&aicwf_usbdrvr) < 0) {
        usb_err("usb_register failed\n");
    }
}

include/linux/usb.h中,Linux定义了usb注册机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/**
 * struct usb_driver - 向 usbcore 标识 USB 接口驱动
 * @name: 驱动名称,在 USB 驱动中应该是唯一的,并且通常与模块名相同。
 * @probe: 被调用来判断该驱动是否愿意管理设备上的特定接口。
 *        如果愿意,probe 返回零,并使用 usb_set_intfdata() 将驱动相关的数据与接口关联。
 *        它也可以使用 usb_set_interface() 来指定合适的备用设置(altsetting)。
 *        如果不愿意管理该接口,返回 -ENODEV;如果发生了真正的 I/O 错误,返回相应的负 errno 值。
 * @disconnect: 当接口不再可访问时被调用,通常因为其设备已经(或正在)被断开连接,
 *              或者驱动模块正在被卸载。
 * @unlocked_ioctl: 供希望通过 "usbfs" 文件系统与用户空间通信的驱动使用。
 *                  这让设备能够提供向用户空间暴露信息的方法,无论它们在文件系统中其他地方
 *                  是否(或是否不)出现。
 * @suspend: 当设备将要被系统挂起时调用,无论是系统休眠还是运行时挂起上下文。
 *           在系统休眠上下文中返回值将被忽略,因此如果挂起失败,不要尝试继续使用设备。
 *           相反,让 resume 或 reset_resume 例程从失败中恢复。
 * @resume: 当设备被系统恢复时调用。
 * @reset_resume: 当被挂起的设备被复位而不是恢复时调用。
 * @pre_reset: 当设备即将被复位时由 usb_reset_device() 调用。
 *            该例程必须等待直到驱动对该设备没有活跃的 URB 才能返回,
 *            并且在 post_reset 方法被调用之前不能再提交任何 URB。
 * @post_reset: 设备复位之后由 usb_reset_device() 调用。
 * @shutdown: 在系统关机时调用,使设备静止。
 * @id_table: USB 驱动使用 ID 表来支持热插拔。
 *            使用 MODULE_DEVICE_TABLE(usb,...) 导出此表。这个字段必须设置,
 *            否则你的驱动的 probe 函数将永远不会被调用。
 * @dev_groups: 绑定到驱动后,将添加到设备的属性组。
 * @dynids: 内部使用,用于保存为此驱动动态添加的设备 ID 列表。
 * @driver: 驱动模型的核心驱动结构。
 * @no_dynamic_id: 如果设为 1,USB 核心将阻止为此驱动添加动态 ID,
 *                即不创建对应的 sysfs 文件。
 * @supports_autosuspend: 如果设为 0,USB 核心将不允许绑定到此驱动的接口进行自动挂起。
 * @soft_unbind: 如果设为 1,USB 核心在调用驱动的 disconnect 方法之前,
 *               不会杀掉 URB 也不会禁用端点。
 * @disable_hub_initiated_lpm: 如果设为 1,USB 核心将不允许集线器在空闲超时发生时
 *                             发起低功耗链路状态转换。设备发起的 USB 3.0 链路 PM 仍然允许。
 *
 * USB 接口驱动必须提供 name、probe() 和 disconnect() 方法,以及一个 id_table。
 * 其他驱动字段是可选的。
 *
 * id_table 用于热插拔。它包含一组描述符,并且可以为每个条目关联特定的数据。
 * 该表同时被用户态和内核态的热插拔支持使用。
 *
 * probe() 和 disconnect() 方法在允许睡眠的上下文中被调用,但不应滥用这一特权。
 * 连接设备的大部分工作应在设备打开时完成,并在最后一次关闭时撤销。
 * disconnect 代码需要处理与 open() 和 close() 方法的并发问题,
 * 以及强制所有未决 I/O 请求完成(通过必要时断开它们,并阻塞直到断开完成)。
 */
struct usb_driver {
	const char *name;

	int (*probe) (struct usb_interface *intf,
		      const struct usb_device_id *id);

	void (*disconnect) (struct usb_interface *intf);

	int (*unlocked_ioctl) (struct usb_interface *intf, unsigned int code,
			void *buf);

	int (*suspend) (struct usb_interface *intf, pm_message_t message);
	int (*resume) (struct usb_interface *intf);
	int (*reset_resume)(struct usb_interface *intf);

	int (*pre_reset)(struct usb_interface *intf);
	int (*post_reset)(struct usb_interface *intf);

	void (*shutdown)(struct usb_interface *intf);

	const struct usb_device_id *id_table;
	const struct attribute_group **dev_groups;

	struct usb_dynids dynids;
	struct device_driver driver;
	unsigned int no_dynamic_id:1;
	unsigned int supports_autosuspend:1;
	unsigned int disable_hub_initiated_lpm:1;
	unsigned int soft_unbind:1;
};


/*
 * 在 module_init()/module_exit() 中使用这些函数,
 * 并且不要忘记 MODULE_DEVICE_TABLE(usb, ...)
 */
extern int usb_register_driver(struct usb_driver *, struct module *,
			       const char *);

/* 使用宏定义以避免包含链来获取 THIS_MODULE 及相关宏 */
#define usb_register(driver) \
	usb_register_driver(driver, THIS_MODULE, KBUILD_MODNAME)

probe是整个驱动最核心的入口之一。 当用户把 AIC8800 USB 网卡插到电脑上时,USB 核心层发现该设备的 ID 匹配成功,就会调用这个 aicwf_usb_probe 函数。在这个函数里,驱动会进行内存分配、初始化网络设备、准备好接收和发送数据。

1.空间申请

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int aicwf_usb_probe(struct usb_interface *intf, const struct usb_device_id *id)
{
    int ret = 0;
    struct usb_device *usb = interface_to_usbdev(intf);
    struct aicwf_bus *bus_if = NULL;
    struct device *dev = NULL;
    struct aicwf_rx_priv* rx_priv = NULL;
    struct aic_usb_dev *usb_dev = NULL;
    #ifdef CONFIG_USB_TX_AGGR
    struct aicwf_tx_priv *tx_priv = NULL;
    #endif

    usb_dev = kzalloc(sizeof(struct aic_usb_dev), GFP_ATOMIC);
    if (!usb_dev) {
        return -ENOMEM;
    }

    usb_dev->udev = usb;
    usb_dev->dev = &usb->dev;
    usb_set_intfdata(x, usb_dev);

usb_set_intfdata(intf, usb_dev):这把新申请的自定义数据 usb_dev 挂靠到了 Linux 标准的 usb_interface 结构体中。这样以后在拔出网卡(disconnect 阶段)时,驱动能随时把它取出来并释放。

2.芯片型号匹配

1
2
3
4
5
6
7
	ret = aicwf_usb_chipmatch(usb_dev, id->idVendor, id->idProduct);
	
	if (ret < 0) {
        AICWFDBG(LOGERROR, "%s pid:0x%04X vid:0x%04X unsupport\n", 
			__func__, id->idVendor, id->idProduct);
        goto out_free_bus;
    }

3.解析USB传输端点

1
2
3
4
5
6
7
8
9
10
11
    ret = aicwf_parse_usb(usb_dev, intf);
    if (ret) {
        AICWFDBG(LOGERROR, "aicwf_parse_usb err %d\n", ret);
        goto out_free;
    }

    ret = aicwf_usb_init(usb_dev);
    if (ret) {
        AICWFDBG(LOGERROR, "aicwf_usb_init err %d\n", ret);
        goto out_free;
    }

aicwf_parse_usb 负责去扫描 USB 设备的描述符,找出哪些是用于接收网络数据的批量输入端点,哪些是用于发送网络数据的批量输出端点。随后 aicwf_usb_init 内部会去初始化 Linux 的 URB(USB Request Block),准备好数据的物理传输载体。

4.总线与数据收发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    bus_if = kzalloc(sizeof(struct aicwf_bus), GFP_ATOMIC);
    if (!bus_if) {
        ret = -ENOMEM;
        goto out_free_usb;
    }

    dev = usb_dev->dev;
    bus_if->dev = dev;
    usb_dev->bus_if = bus_if;
    bus_if->bus_priv.usb = usb_dev;
    dev_set_drvdata(dev, bus_if);

    bus_if->ops = &aicwf_usb_bus_ops;

    rx_priv = aicwf_rx_init(usb_dev);
    if(!rx_priv) {
       AICWFDBG(LOGERROR, "rx init failed\n");
        ret = -1;
        goto out_free_bus;
    }
    usb_dev->rx_priv = rx_priv;

#ifdef CONFIG_USB_TX_AGGR
    tx_priv = aicwf_tx_init(usb_dev);
    if(!tx_priv) {
        usb_err("tx init fail\n");
        goto out_free_bus;
    }
    usb_dev->tx_priv = tx_priv;
    aicwf_frame_queue_init(&tx_priv->txq, 8, TXQLEN);
    spin_lock_init(&tx_priv->txqlock);
    spin_lock_init(&tx_priv->txdlock);
#endif

bus_if:AIC 的驱动架构支持多种接口。这里抽象出一个 bus_if 并绑定 aicwf_usb_bus_ops,使得上层的无线网络层不需要关心底层到底是 USB 还是 SDIO。

rx_inittx_init:初始化网卡的接收和发送队列。

CONFIG_USB_TX_AGGR:这是一个条件编译宏。如果开启了 TX Aggregation(发送聚合),驱动会把多个小的网络数据包打包成一个大包一次性发给 USB,这能大幅提升 Wi-Fi 的吞吐量。里面还初始化了自旋锁(spin_lock_init)防止多核多线程冲突。

5.固件下载、总线启动、开机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    ret = aicwf_bus_init(0, dev);
    if (ret < 0) {
        AICWFDBG(LOGERROR, "aicwf_bus_init err %d\n", ret);
        goto out_free_bus;
    }

    ret = aicwf_bus_start(bus_if);
    if (ret < 0) {
        AICWFDBG(LOGERROR, "aicwf_bus_start err %d\n", ret);
        goto out_free_bus;
    }

    ret = aicwf_rwnx_usb_platform_init(usb_dev);
	if (ret < 0) {
        AICWFDBG(LOGERROR, "aicwf_rwnx_usb_platform_init err %d\n", ret);
        goto out_free_bus;
    }
    aicwf_hostif_ready();

#ifdef CONFIG_GPIO_WAKEUP
	rwnx_register_hostwake_irq(usb_dev->dev);
#endif

    return 0;

aicwf_bus_start 会让网卡开始监听 USB 总线,准备接收数据。

aicwf_rwnx_usb_platform_init:负责向网卡芯片下载固件,并初始化底层的 mac80211/cfg80211。

aicwf_hostif_ready():正式通知系统网卡的主机接口已准备好,可以开始创建 wlan0 网卡接口了。

6.异常回滚

1
2
3
4
5
6
7
8
9
10
11
out_free_bus:
    aicwf_bus_deinit(dev);
    kfree(bus_if);
out_free_usb:
    aicwf_usb_deinit(usb_dev);
out_free:
    usb_err("failed with errno %d\n", ret);
    kfree(usb_dev);
    usb_set_intfdata(intf, NULL);
    return ret;
}

如果在前边的步骤比如初始化 TX 失败了,代码就会通过 goto 跳到对应的标签。前边申请了什么,这里就倒着释放什么,保证失败退出时不会出现内存泄漏、系统死机。

This post is licensed under CC BY 4.0 by the author.