在上一章Linux模块(2) - 创建设备节点已经将设备节点创建好了,可以进行用户态程序的交互了,今天完善一下资源映射的动作。
1. 资源定义
第一章Linux模块(1) - 加载与卸载中我们定义了struct platform_device mac_platform_device
结构体,但是并未定义该设备所需要的资源有哪些,比如寄存器
中断
等。Linux内核集中托管了物理中断和内存映射,对内提供虚拟地址空间和虚拟中断号,我们需要先定义所需的物理资源,然后向内核申请对应的虚拟资源。
1.1 资源头文件
mac资源涉及的资源信息较多,我们定义一个头文件专门放置相关的信息。
#ifndef _MAC_MODULE_H_
#define _MAC_MODULE_H_
// 系统控制寄存器地址,控制I/O复用和MAC模式等信息
#define SYSCTRL_ADDR_SIZE 0x1000
#define SYSCTRL_BASE_ADDR 0xA0400000
// MAC配置寄存器地址
#define MAC_ADDR_SIZE 0x2000
#define MAC_BASE_ADDR 0xA0600000
// MDIO操作PHY的地址
#define MAC_PHY_ADDR 0
// MAC中断
#define MAC_IRQ_NUM 36
// 资源相关名称的定义
#define SYSCTRL_BASE_NAME "sysctrl"
#define MAC_BASE_NAME "base"
#define MAC_PHY_ADDR "phy-addr"
#define MAC_IRQ_NAME "irq"
// 模块等名称定义,用于将之前在源码中写的字符串替换成对应的宏
#define MODULE_NAME "mac"
#define DRIVER_NAME MODULE_NAME
#define DEVICE_NAME MODULE_NAME
#define MODULE_MAC_IRQ_NAME MODULE_NAME":"MAC_IRQ_NAME
// 设置模块为杂项设备,也可以注释掉这个定义,使用普通的设备号
#define MODULE_MISC_DEVICE
#endif /* _MAC_MODULE_H_ */
1.2 资源结构体
struct platform_device
中有两个成员和资源相关。
// include/linux/platform_device.h
struct platform_device {
...
u32 num_resources; // 资源的数量,会用到ARRAY_SIZE()宏,该宏能够进行代码静态检查
struct resource *resource; // 资源数组
...
}
// include/linux/ioport.h
struct resource {
resoucre_size_t start; // u32
resource_size_t end; // 通常需要减1,比如0x200000000+0x2000-1
const char *name; // 资源可以通过索引查找也可以通过名称查找,索引更快,但与资源定义的相对位置强相关
unsigned long flags; // 这里的一部分位段存储了资源的类型信息,资源查找时的必匹配项之一
// IORESOURCE_BITS --- 0x000000FF --- 细分字段
// IORESOURCE_IRQ_HIGHDGE --- 上升沿中断
// IORESOURCE_IRQ_xxx
// IORESOURCE_DMA_8BIT --- DMA8位宽
...
// 类型
// IORESOURCE_TYPE_BITS --- 0x00001F00 --- 类型字段
// IORESOURCE_IO --- PCI/ISA等内存I/O,需要通过专用接口或指令进行访问,无法通过总线直接访问,嵌入式中很少用
// IORESOURCE_MEM --- 内存,可通过总线直接访问
// IORESOURCE_REG --- 寄存器的偏移
// IORESOURCE_IRQ --- 中断号
// IORESOURCE_DMA --- DMA索引
// IORESOURCE_BUS --- 总线索引
// 访问特性
// IORESOURCE_PREFETCH --- 支持预取
// IORESOURCE_READONLY --- 只读
// IORESOURCE_CACHEABLE --- 支持缓冲
// IORESOURCE_RANGELENGTH --- 范围
// ...
unsigned long desc;
struct resource *parent, *sibling, *child; // 父,兄,子,作为树的节点插入树中
}
定义资源结构体。
static struct resource mac_resource[] = {
{
.name = SYSCTRL_BASE_NAME,
.start = SYSCTRL_BASE_ADDR,
.end = SYSCTRL_BASE_ADDR+SYSCTRL_ADDR_SIZE-1,
.flags = IORESOURCE_MEM,
}, {
.name = MAC_BASE_NAME,
.start = MAC_BASE_ADDR,
.end = MAC_BASE_ADDR+MAC_ADDR_SIZE-1,
.flags = IORESOURCE_MEM,
}, {
.name = MAC_PHY_NAME,
.start = MAC_PHY_ADDR,
.flags = IORESOURCE_REG, // 暂且使用IORESOURCE_REG这个类型,使用时只需要读取start的值即可
}, {
.name = MAC_IRQ_NAME,
.start = MAC_IRQ_NUM,
.flags = IORESOURCE_IRQ, // 中断类型,只有start有效
},
};
static struct platform_device mac_platform_device = {
.name = DEVICE_NAME, // 使用宏替代字符串
.id = 0,
.dev = {
.release = mac_release,
},
.num_resources = ARRAY_SIZE(mac_resource); // 使用ARRAY_SIZE()宏
.resource = mac_resource, // 资源结构体数组
};
2. 资源获取
内核中资源的定义和获取是相互独立的,资源的定义可以通过struct platform_device
结构体,也可以通过设备树,对于资源获取内核也提供了一组接口。
2.1 函数接口
// drivers/base/platform.c
// 查找第num个匹配type的资源信息
struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num)
{
int i;
for (i = 0; i < dev->num_resources; i++) { // 逐一对比
struct resource *r = &dev->resource[i];
if (type == resource_type(r) && num-- == 0) // 前面匹配类型,后面匹配数量
return r;
}
return NULL; // 失败反回NULL
}
// 查找第num个中断资源
int platform_get_irq(struct platform_device *dev, unsigned int num)
{
struct resource *r;
if (IS_ENABLED(CONFIG_OF_IRQ) && dev->dev.of_node) { // 通过设备树设置的中断信息
...
ret = of_irq_get(dev->dev.of_node, num);
{
...
return irq_create_of_mapping(&oirq); // 执行中断映射后返回
}
if (ret > 0 || ret == -EPROBE_DEFER) { // 虚拟中断号必须大于0?
return ret; // 成功或-EPROBE_DEFER则返回,否则后面继续查找
}
}
r = platform_get_resource(dev, IORESOURCE_IRQ, num); // 就是个普通的查找
if (r && r->flags & IORESOURCE_BITS) {
...
irqd_set_trigger_type(...); // 若设置了资源的中断模式,则会运行到这里
}
return r ? r->start : -ENXIO; // 返回值应判断是否<0
}
// 根据类型和名称查找
struct resource *platform_get_resource_byname(struct platform_device *dev, unsigned int type, const char *name)
{
...
if (type == resource_type(r) && !strcmp(r->name, name)) // 数字比较变成了字符串比较
return r;
...
}
2.2 probe
同样的我们需要在probe()
函数中进行资源的获取,基于Linux模块(2) - 创建设备节点进行完善。
int mac_probe(struct platform_device *pdev)
{
struct driver_info *mac;
int ret, instance_num;
struct device *device;
struct resource *res; // 新增
// 使用devm_前缀的函数,能够自动进行内存的释放,当然也可以手动释放
// 申请一个结构体用于标记我们申请的各种内存,以便最后释放
mac = mdev_kzalloc(&pdev->dev, sizeof(struct driver_info), GFP_KERNEL); // 错误返回NULL
if (unlikely(mac == NULL)) { // 预期不会出现
de_err(&pdev->dev, "alloc mac failed\n"); // 内核日志函数,具备打印等级控制的功能
ret = -ENOMEM; // 内存不足
goto go_ret; // 驱动加载过程较多故需要采用goto语句,逐层返回
}
/******************************************************/
res = platform_get_resource_byname(pdev, IORESOURCE_MEM, SYSCTRL_BASE_NAME);
if (unlikely(res == NULL)) {
de_err(&pdev->dev, "get resource[%s] failed\n", SYSCTRL_BASE_NAME); // 采用%s虽然运行时间更久,但能够节省rodata空间,而且这是个不期望的错误打印,故不将SYSCTRL_BASE_NAME在%s处直接展开
ret = -ENXIO; // IO资源错误
goto go_kfree_mac;
}
// 因为SYSCTRL这块物理空间也被其他地方映射过,所以使用devm_ioremap_resource()会失败,使用devm_ioremap()即可。devm_ioremap_resource()会先申请这块物理地址,然后调用devm_ioremap()进行虚拟地址空间的映射,而物理地址空间只能申请一次,所以会失败,只要内核申请过这块物理空间就可以映射成功。
//mac->sysctrl_base = devm_ioremap_resource(&pdev->dev, res); // 返回值使用IS_ERR()判断
// resource_size() <==> (res->end - res->start + 1)
mac->sysctrl_base = devm_ioremap(&pdev->dev, res->start, resource_size(res)); // 失败返回NULL
if (IS_ERR_OR_NULL(mac->sysctrl_base)) { // 暂且检查IS_ERR和NULL,这样以上两个函数都可检查到位
de_err(&pdev->dev, "ioremap[%s:01x%08x:0x08x] failed\n", SYSCTRL_BASE_NAME, res->start, resource_size(res));
ret = -ENOMEM; // 内存不足
goto go_kfree_mac;
}
res = platform_get_resource_byname(pdev, IORESOURCE_MEM, MAC_BASE_NAME);
if (unlikely(res == NULL)) {
de_err(&pdev->dev, "get resource[%s] failed\n", MAC_BASE_NAME);
ret = -ENXIO;
goto go_iounmap_sysctrl_base;
}
mac->base = devm_ioremap_resource(&pdev->dev, res); // 返回值使用IS_ERR()判断
if (IS_ERR(mac->base)) {
de_err(&pdev->dev, "ioremap[%s:01x%08x:0x08x] failed\n", MAC_BASE_NAME, res->start, resource_size(res));
ret = -ENOMEM;
goto go_iounmap_sysctrl_base;
}
res = platform_get_resource_byname(pdev, IORESOURCE_REG, MAC_PHY_NAME); // 这里IORESOURCE_REG和定义资源结构体时对应
if (unlikely(res == NULL)) {
de_err(&pdev->dev, "get resource[%s] failed\n", MAC_PHY_NAME);
ret = -ENXIO;
goto go_iounmap_base;
}
mac->phy_addr = res->start; // 这里和定义资源结构体时对应
ret = platform_get_irq_byname(pdev, MAC_IRQ_NAME); // 错误返回负值
if (unlikely(ret < 0)) {
de_err(&pdev->dev, "get irq[%s] failed\n", MAC_IRQ_NAME);
goto go_iounmap_base;
}
mac->irq = irq_create_mapping(NULL, ret); // 手动定义的资源数据,platform_get_irq_byname()不会自动映射,需要手动映射,这里NULL会使用默认的irq_default_domain,我使用的平台只有一个中断控制器应该是OK的,后面再理解吧
// 注册中断回调函数mac_irq_handler(),错误返回负值,mac参数将作为参数传递给mac_irq_handler()使用,在使用共享中断时devm_request_irq()最后一个参数不能是NULL
ret = devm_request_irq(&pdev->dev, mac->irq, mac_irq_handler, 0, MODULE_MAC_IRQ_NAME, mac);
if (unlikely(ret < 0)) {
de_err(&pdev->dev, "request irq[%s] failed\n", MODULE_MAC_IRQ_NAME);
goto go_unmap_irq;
}
/******************************************************/
instance_num = atomic_add_return(1, &mac_instance_num) - 1; // 获取当前已注册设备的数量,从0开始,后续若注册失败也不再回减,以免发生冲突
#ifdef MODULE_MISC_DEVICE // 杂项设备注册流程
mac->miscdev.minor = MISC_DYNAMIC_MINOR; // 设置次设备号进行动态分配,当然也可以手动指定
mac->miscdev.fops = &mac_file_operations; // 设备文件操作接口,我们稍后定义
mac->miscdev.parent = &pdev_dev; // 暂不清楚作用
// C库中也有一个asprintf()函数作用类似
mac->miscdev.name = devm_kasprintf(&pdev->dev, GFP_KERNEL, "%s%d", "mac", instance_num); // 申请一块内存,用于存放格式化的字符串,该字符串用于设备文件名,错误返回NULL
if (unlikely(mac->miscdev.name == NULL)) {
de_err(&pdev->dev, "alloc miscdev name failed\n");
ret = -ENOMEM; // 内存不足
goto go_free_irq; // 新修改
}
ret = misc_register(&mac->miscdev); // 杂项设备注册,返回值使用IS_ERR()检查
if (IS_ERR(ret)) {
de_err(&pdev->dev, "misc register[%s] failed\n", mac->miscdev.name);
goto go_kfree_name;
}
#else // 以下是普通设备的注册流程
cdev_init(&mac->cdev, &mac_file_operations); // 静态初始化cdev,设备文件操作接口我们稍后定义
mac->cdev.owner = THIS_MODULE; // 指定当前模块为cdev拥有者,THIS_MODULE,是一个结构体指针
ret = alloc_chrdev_region(&mac->cdev.dev, instance_num, 1, "mac"); // 申请设备号,当然也可以不用该函数,手动指定
if (IS_ERR(ret)) {
de_err(&pdev->dev, "alloc chrdev failed\n");
goto go_free_irq; // 新修改
}
ret = cdev_add(&mac->cdev, mac->cdev.dev, 1); // 注册cdev
if (IS_ERR(ret)) {
de_err(&pdev->dev, "cdev add failed\n");
goto go_unregister_chrdev;
}
mac->class = class_create(THIS_MODULE, "mac"); // 创建class,用于设备文件的创建
if (IS_ERR(mac->class)) {
de_err(&pdev->dev, "class create failed\n");
goto go_cdev_del;
}
// 创建设备文件,文件名为mac%u格式化后的字符串
device = device_create(mac->class, NULL, mac->cdev.dev, NULL, "mac%u", instance_num);
if (IS_ERR(device)) {
de_err(&pdev->dev, "device create failed\n");
ret = PTR_ERR(device);
goto go_class_destroy;
}
#endif
platform_set_drvdata(pdev, mac); // 等同于pdev->dev.driver_data = mac;
dev_info(&pdev->dev, "mac");
return 0;
#ifdef MODULE_MISC_DEVICE
goto go_misc_deregister; // 不会运行到这
go_misc_deregister:
misc_deregister(&mac->miscdev);
go_kfree_name:
devm_kfree(&pdev->dev, (void *)mac->miscdev.name);
#else
goto go_device_destory; // 不会运行到这
go_device_destory:
device_destory(mac->class, mac->cdev.dev); // mac->cdev.dev是设备号
go_class_destroy:
class_destroy(mac->class);
go_cdev_del:
cdev_del(&mac->cdev);
go_unregister_chrdev:
unregister_chrdev_region(mac->cdev.dev, 1); // mac->cdev.dev是设备号
#endif
go_free_irq:
devm_free_irq(&pdev->dev, mac->irq, mac);
go_unmap_irq:
irq_dispose_mapping(mac->irq); // 暂时不确定用法是否正确
go_iounmap_base:
devm_iounmap(&pdev->dev, mac->base);
go_iounmap_sysctrl_base:
devm_iounmap(&pdev->dev, mac->sysctrl_base);
go_kfree_mac:
devm_kfree(&pdev->dev, mac);
go_ret:
return ret;
}
2.3 remove
mac_remove()
函数需要同样做些修改。
int mac_remove(struct platform_device *pdev)
{
struct driver_info *mac = platform_get_drvdata(pdev); // 等同于pdev->dev.driver_data
#ifdef MODULE_MISC_DEVICE
misc_deregister(&mac->miscdev);
devm_kfree(&pdev->dev, (void *)mac->miscdev.name);
#else
device_destory(mac->class, mac->cdev.dev); // mac->cdev.dev是设备号
class_destroy(mac->class);
cdev_del(&mac->cdev);
unregister_chrdev_region(mac->cdev.dev, 1); // mac->cdev.dev是设备号
#endif
devm_free_irq(&pdev->dev, mac->irq, mac);
irq_dispose_mapping(mac->irq); // 暂时不确定用法是否正确
devm_iounmap(&pdev->dev, mac->base);
devm_iounmap(&pdev->dev, mac->sysctrl_base);
devm_kfree(&pdev->dev, mac);
}
3. 其他
3.1 全局数据
我们在mac_probe()
中对struce device_info
增加了一些成员,这里完善一下。
#include <linux/misdevice.h>
#include <linux/cdev.h>
struct driver_info { // 自定义结构体类型
#ifdef MODULE_MISC_DEVICE // 将模块作为杂项设备自动注册
struct miscdevice miscdev; // 杂项设备结构体,会自动创建设备节点
#else // 将模块作为普通设备自动注册
struct cdev cdev; // 设备结构体,可以使用动态内存申请,也可以静态定义
struct class *class; // 用于自动创建设备节点
#endif
/**********************************/
void __iomem *sysctrl_base; // 裸驱操作时的寄存器位置
void __iomem *base;
uint32_t phy_addr; // MDIO操作PHY时记录PHY的地址
uint32_t irq; // 中断释放的时候需要使用
/**********************************/
}
3.2 中断服务函数
// 参数p,即struct driver_info *mac
irqreturn_t mac_irq_handler(int irq, void *p)
{
printk("%s\n", __func__);
return IRQ_HANDLED;
}