天津网站建设哪家好,3d建模师可以自学吗,有做网站看病的吗,展厅布置摆放设计公司在之前的字符设备程序中驱动程序#xff0c;我们只要调用open() 函数打开了相应的设备文件#xff0c;就可以使用read()/write() 函数#xff0c;通过file_operations 这个文件操作接口来进行硬件的控制。这种驱动开发方式简单直观#xff0c;但是从软件设计的角度看#…在之前的字符设备程序中驱动程序我们只要调用open() 函数打开了相应的设备文件就可以使用read()/write() 函数通过file_operations 这个文件操作接口来进行硬件的控制。这种驱动开发方式简单直观但是从软件设计的角度看却是一种十分糟糕的方式。
它有一个严重的问题就是设备信息和驱动代码杂糅在一起在我们驱动程序中各种硬件寄存器地址随处可见。本质上这种驱动开发方式与单片机的驱动开发并没有太大的区别一旦硬件信息发生变化甚至设备已经不在了就必须要修改驱动源码。我们之前做的事情只不过是简单地给它套了一个文件操作接口的外壳。
Linux 作为一个发展成熟、功能齐全、结构复杂的操作系统它对于代码的可维护性、复用性非常看重。如果它放任每位驱动开发人员任凭自己的个人喜好来进行驱动代码开发最终必将导致内核充斥着大量冗余、无意义的驱动代码给linux 内核的迭代开发带来巨大的维护成本。
为了解决这种驱动代码和设备信息耦合的问题linux 提出了设备驱动模型。前面章节我们已经对设备驱动模型进行了深入剖析在设备驱动模型中引入总线的概念可以对驱动代码和设备信息进行分离。但是驱动中总线的概念是软件层面的一种抽象与我们SOC 中物理总线的概念并不严格相等
物理总线芯片与各个功能外设之间传送信息的公共通信干线其中又包括数据总线、地址总线和控制总线以此来传输各种通信时序。驱动总线负责管理设备和驱动。制定设备和驱动的匹配规则一旦总线上注册了新的设备或者是新的驱动总线将尝试为它们进行配对。
一般对于I2C、SPI、USB 这些常见类型的物理总线来说Linux 内核会自动创建与之相应的驱动总线因此I2C 设备、SPI 设备、USB 设备自然是注册挂载在相应的总线上。但是实际项目开发中还有很多结构简单的设备对它们进行控制并不需要特殊的时序。它们也就没有相应的物理总线比如led、rtc 时钟、蜂鸣器、按键等等Linux 内核将不会为它们创建相应的驱动总线。
为了使这部分设备的驱动开发也能够遵循设备驱动模型Linux 内核引入了一种虚拟的总线——平台总线platform bus)。平台总线用于管理、挂载那些没有相应物理总线的设备这些设备被称为平台设备对应的设备驱动则被称为平台驱动。平台设备驱动的核心依然是Linux 设备驱动模型平台设备使用platform_device 结构体来进行表示其继承了设备驱动模型中的device 结构体。而平台驱动使用platform_driver 结构体来进行表示其则是继承了设备驱动模型中的device_driver结构体。
平台总线
平台总线注册和匹配方式
在Linux 的设备驱动模型中总线是最重要的一环。上一节中我们提到过总线是负责匹配设备和驱动它维护着两个链表里面记录着各个已经注册的平台设备和平台驱动。每当有新的设备或者是新的驱动加入到总线时总线便会调用platform_match 函数对新增的设备或驱动进行配对。内核中使用bus_type 来抽象描述系统中的总线平台总线结构体原型如下所示
列表1: platform_bus_type 结构体(内核源码/driver/base/platform.c)
struct bus_type platform_bus_type {.name platform,
.dev_groups platform_dev_groups,
.match platform_match,
.uevent platform_uevent,
.pm platform_dev_pm_ops,};EXPORT_SYMBOL_GPL(platform_bus_type);内核用platform_bus_type 来描述平台总线该总线在linux 内核启动的时候自动进行注册。
列表2: platform_bus_init 函数(内核源码/driver/base/platform.c)
int __init platform_bus_init(void)
{int error;...error bus_register(platform_bus_type);...return error;
}第5 行向linux 内核注册platform 平台总线
这里重点是platform 总线的match 函数指针该函数指针指向的函数将负责实现平台总线和平台设备的匹配过程。对于每个驱动总线它都必须实例化该函数指针。platform_match 的函数原型如下
列表3: platform_match 函数(内核源码/driver/base/platform.c)
static int platform_match(struct device *dev, struct device_driver *drv)
{struct platform_device *pdev to_platform_device(dev);struct platform_driver *pdrv to_platform_driver(drv);/* When driver_override is set, only bind to the matching driver */if (pdev-driver_override)return !strcmp(pdev-driver_override, drv-name);/* Attempt an OF style match first */if (of_driver_match_device(dev, drv))return 1;/* Then try ACPI style match */if (acpi_driver_match_device(dev, drv))return 1;/* Then try to match against the id table */if (pdrv-id_table)return platform_match_id(pdrv-id_table, pdev) ! NULL;/* fall-back to driver name match */return (strcmp(pdev-name, drv-name) 0);}第4-5 行这里调用了to_platform_device() 和to_platform_driver() 宏。这两个宏定义的原型如下
列表4: to_platform_xxx 宏定义(内核源码/include/linux/platform_device.h)
#define to_platform_device(x) (container_of((x), struct platform_device, dev)
#define to_platform_driver(drv) (container_of((drv), struct platform_driver, driver))其中to_platform_device 和to_platform_driver 实现了对container_of 的封装dev、driver 分别作为platform_device、platform_driver 的成员变量通过container_of 宏可以获取到正在进行匹配的platform_driver 和platform_device。
第8-21 行platform 总线提供了四种匹配方式并且这四种方式存在着优先级设备树机制ACPI 匹配模式id_table 方式 字符串比较。虽然匹配方式五花八门但是并没有涉及到任何复杂的算法都只是在匹配的过程中比较一下设备和驱动提供的某个成员的字符串是否相同。设备树是一种描述硬件的数据结构它用一个非C 语言的脚本来描述这些硬件设备的信息。驱动和设备之间的匹配时通过比较compatible 的值。acpi 主要是用于电源管理基本上用不到这里就并不进行讲解了。关于设备树的匹配机制会在设备树章节进行详细分析。
id_table 匹配方式
在这一章节我们先来分析平台总线id_table 匹配方式在定义结构体platform_driver 时我们需要提供一个id_table 的数组该数组说明了当前的驱动能够支持的设备。当加载该驱动时总线的match 函数发现id_table 非空则会比较id_table 中的name 成员和平台设备的name 成员若相同则会返回匹配的条目具体的实现过程如下
列表5: platform_match_id 函数(内核源码/drivers/base/platform.c)
static const struct platform_device_id *platform_match_id(const struct platform_device_id *id,struct platform_device *pdev){while (id-name[0]) {if (strcmp(pdev-name, id-name) 0) {pdev-id_entry id;return id;}id;}return NULL;
}大家可以看到这里的代码实现并不复杂只是通过字符串进行配对。每当有新的驱动或者设备添加到总线时总线便会调用match 函数对新的设备或者驱动进行配对。platform_match_id 函数中第一个参数为驱动提供的id_table第二个参数则是待匹配的平台设备。当待匹配的平台设备的name 字段的值等于驱动提供的id_table 中的值时会将当前匹配的项赋值给platform_device 中的id_entry返回一个非空指针。若没有成功匹配则返回空指针。 倘若我们的驱动没有提供前三种方式的其中一种那么总线进行匹配时只能比较platform_device中的name 字段以及嵌在platform_driver 中的device_driver 的name 字段。 平台设备
platform_device 结构体
内核使用platform_device 结构体来描述平台设备结构体原型如下
列表6: platform_device 结构体(内核源码/include/linux/platform_device.h)
struct platform_device {const char *name;int id;struct device dev;u32 num_resources;struct resource *resource;const struct platform_device_id *id_entry;/* 省略部分成员*/
};name设备名称总线进行匹配时会比较设备和驱动的名称是否一致id指定设备的编号Linux 支持同名的设备而同名设备之间则是通过该编号进行区分devLinux 设备模型中的device 结构体linux 内核大量使用了面向对象思想platform_device通过继承该结构体可复用它的相关代码方便内核管理平台设备num_resources记录资源的个数当结构体成员resource 存放的是数组时需要记录resource数组的个数内核提供了宏定义ARRAY_SIZE 用于计算数组的个数resource平台设备提供给驱动的资源如irqdma内存等等。该结构体会在接下来的内容进行讲解id_entry平台总线提供的另一种匹配方式原理依然是通过比较字符串这部分内容会在平台总线小节中讲这里的id_entry 用于保存匹配的结果
何为设备信息
平台设备的工作是为驱动程序提供设备信息, 设备信息包括硬件信息和软件信息两部分。
硬件信息驱动程序需要使用到什么寄存器占用哪些中断号、内存资源、IO 口等等软件信息以太网卡设备中的MAC 地址、I2C 设备中的设备地址、SPI 设备的片选信号线等等
对于硬件信息使用结构体struct resource 来保存设备所提供的资源比如设备使用的中断编号寄存器物理地址等结构体原型如下
列表7: resource 结构体(内核源码/include/linux/ioport.h)
/*
2 * Resources are tree-like, allowing
3 * nesting etc..
4 */struct resource {resource_size_t start;resource_size_t end;const char *name;unsigned long flags;/* 省略部分成员*/
};name指定资源的名字可以设置为NULLstart、end指定资源的起始地址以及结束地址flags用于指定该资源的类型在Linux 中资源包括I/O、Memory、Register、IRQ、DMA、Bus 等多种类型最常见的有以下几种
资源宏定义描述IORESOURCE_IO用于IO 地址空间对应于IO 端口映射方式IORESOURCE_MEM用于外设的可直接寻址的地址空间IORESOURCE_IRQ用于指定该设备使用某个中断IORESOURCE_DMA用于指定使用的DMA 通道
设备驱动程序的主要目的是操作设备的寄存器。不同架构的计算机提供不同的操作接口主要有IO 端口映射和IO 存映射两种方式。对应于IO 端口映射方式只能通过专门的接口函数如inb、outb才能访问采用IO 内存映射的方式可以像访问内存一样去读写寄存器。在嵌入式中基本上没有IO 地址空间所以通常使用IORESOURCE_MEM。
在资源的起始地址和结束地址中对于IORESOURCE_IO 或者是IORESOURCE_MEM他们表示要使用的内存的起始位置以及结束位置若是只用一个中断引脚或者是一个通道则它们的start 和end 成员值必须是相等的。
而对于软件信息这种特殊信息需要我们以私有数据的形式进行封装保存我们注意到platform_device 结构体中有个device 结构体类型的成员dev。在前面章节我们提到过Linux 设备模型使用device 结构体来抽象物理设备该结构体的成员platform_data 可用于保存设备的私有数据。platform_data 是void * 类型的万能指针无论你想要提供的是什么内容只需要把数据的地址赋值给platform_data 即可还是以GPIO 引脚号为例示例代码如下
列表8: 示例代码
unsigned int pin 10;struct platform_device pdev {.dev {.platform_data pin;}
}将保存了GPIO 引脚号的变量pin 地址赋值给platform_data 指针在驱动程序中通过调用平台设备总线中的核心函数可以获取到我们需要的引脚号。
注册/注销平台设备
当我们定义并初始化好platform_device 结构体后需要把它注册、挂载到平台设备总线上。注册平台设备需要使用platform_device_register() 函数该函数原型如下
列表9: platform_device_register 函数(内核源码/drivers/base/platform.c)
int platform_device_register(struct platform_device *pdev)函数参数和返回值如下
参数 pdev: platform_device 类型结构体指针 返回值
成功 0失败负数
同样当需要注销、移除某个平台设备时我们需要使用platform_device_unregister 函数来通知平台设备总线去移除该设备。
列表10: platform_device_unregister 函数(内核源码/drivers/base/platform.c)
void platform_device_unregister(struct platform_device *pdev)函数参数和返回值如下
参数 pdev: platform_device 类型结构体指针
返回值无
到这里平台设备的知识已经讲解完毕平台设备的主要内容是将硬件部分的代码与驱动部分的代码分开注册到平台设备总线中。平台设备总线为设备和驱动之间搭建了一座桥——统一的数据结构以及函数接口设备和驱动的数据交互直接在“这座桥上”进行。
平台驱动
platform_driver 结构体
内核中使用platform_driver 结构体来描述平台驱动结构体原型如下所示
列表11: platform_driver 结构体(内核源码/include/platform_device.h)
struct platform_driver {int (*probe)(struct platform_device *);int (*remove)(struct platform_device *);struct device_driver driver;const struct platform_device_id *id_table;};probe函数指针驱动开发人员需要在驱动程序中初始化该函数指针当总线为设备和驱动匹配上之后会回调执行该函数。我们一般通过该函数对设备进行一系列的初始化。remove函数指针驱动开发人员需要在驱动程序中初始化该函数指针当我们移除某个平台设备时会回调执行该函数指针该函数实现的操作通常是probe 函数实现操作的逆过程。driver Linux 设备模型中用于抽象驱动的device_driver 结构体platform_driver 继承该结构体也就获取了设备模型驱动对象的特性id_table表示该驱动能够兼容的设备类型。
platform_device_id 结构体原型如下所示:
列表12: id_table 结构体(内核源码/include/linux/mod_devicetable.h)
struct platform_device_id {char name[PLATFORM_NAME_SIZE];kernel_ulong_t driver_data;};在platform_device_id 这个结构体中有两个成员第一个是数组用于指定驱动的名称总线进行匹配时会依据该结构体的name 成员与platform_device 中的变量name 进行比较匹配另一个成员变量driver_data则是用于来保存设备的配置。我们知道在同系列的设备中往往只是某些寄存器的配置不一样为了减少代码的冗余尽量做到一个驱动可以匹配多个设备的目的。接下来以imx 的串口为例具体看下这个结构体的作用
列表13: 示例代码(内核源码/drivers/tty/serial/imx.c)
static struct imx_uart_data imx_uart_devdata[] {[IMX1_UART] {.uts_reg IMX1_UTS,.devtype IMX1_UART,},[IMX21_UART] {.uts_reg IMX21_UTS,.devtype IMX21_UART,},[IMX6Q_UART] {.uts_reg IMX21_UTS,.devtype IMX6Q_UART,},};static struct platform_device_id imx_uart_devtype[] {{.name imx1-uart,.driver_data (kernel_ulong_t) imx_uart_devdata[IMX1_UART],},{.name imx21-uart,.driver_data (kernel_ulong_t) imx_uart_devdata[IMX21_UART],},{.name imx6q-uart,.driver_data (kernel_ulong_t) imx_uart_devdata[IMX6Q_UART],},{/* sentinel */}};第1-18 行: 声明了一个结构体数组用来表示不同平台的串口类型。第20-42 行: 使用platform_device_id 结构体中的driver_data 成员来储存上面的串口信息
在上面的代码中支持三种设备的串口支持imx1、imx21、imx6q 三种不同系列芯片他们之间区别在于串口的test 寄存器地址不同。当总线成功配对平台驱动以及平台设备时会将对应的id_table 条目赋值给平台设备的id_entry 成员而平台驱动的probe 函数是以平台设备为参数这样的话就可以拿到当前设备串口的test 寄存器地址了。
注册/注销平台驱动
当我们初始化了platform_driver 之后通过platform_driver_register() 函数来注册我们的平台驱动该函数原型如下
列表14: platform_driver_register 函数
int platform_driver_register(struct platform_driver *drv);函数参数和返回值如下
参数 drv: platform_driver 类型结构体指针
返回值
成功 0失败负数
由于platform_driver 继承了driver 结构体结合Linux 设备模型的知识当成功注册了一个平台驱动后就会在/sys/bus/platform/driver 目录下生成一个新的目录项。
当卸载的驱动模块时需要注销掉已注册的平台驱动platform_driver_unregister() 函数用于注销已注册的平台驱动该函数原型如下
列表15: platform_driver_unregister 函数(内核源码/drivers/base/platform.c)
void platform_driver_unregister(struct platform_driver *drv);参数 drv: platform_driver 类型结构体指针
返回值无
上面所讲的内容是最基本的平台驱动框架只需要实现probe 函数、remove 函数初始化platform_driver 结构体并调用platform_driver_register 进行注册即可。
平台驱动获取设备信息
在学习平台设备的时候我们知道平台设备使用结构体resource 来抽象表示硬件信息而软件信息则可以利用设备结构体device 中的成员platform_data 来保存。先看一下如何获取平台设备中结构体resource 提供的资源。
platform_get_resource() 函数通常会在驱动的probe 函数中执行用于获取平台设备提供的资源结构体最终会返回一个struct resource 类型的指针该函数原型如下
列表16: platform_get_resource 函数
struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num);参数
dev指定要获取哪个平台设备的资源type指定获取资源的类型如IORESOURCE_MEM、IORESOURCE_IO 等num指定要获取的资源编号。每个设备所需要资源的个数是不一定的为此内核对这些资源进行了编号对于不同的资源编号之间是相互独立的。
返回值
成功 struct resource 结构体类型指针失败 NULL
假若资源类型为IORESOURCE_IRQ平台设备驱动还提供以下函数接口来获取中断引脚
列表17: platform_get_irq 函数
int platform_get_irq(struct platform_device *pdev, unsigned int num)参数
pdev指定要获取哪个平台设备的资源num指定要获取的资源编号。
返回值
成功可用的中断号失败负数
对于存放在device 结构体中成员platform_data 的软件信息我们可以使用dev_get_platdata 函数
来获取函数原型如下所示
static inline void *dev_get_platdata(const struct device *dev)
{return dev-platform_data;
}参数
dev struct device 结构体类型指针
返回值 device 结构体中成员platform_data 指针
以上几个函数接口就是如何从平台设备中获取资源的常用的几个函数接口到这里平台驱动部分差不多就结束了。总结一下平台驱动需要实现probe 函数当平台总线成功匹配驱动和设备时则会调用驱动的probe 函数在该函数中使用上述的函数接口来获取资源以初始化设备最后填充结构体platform_driver调用platform_driver_register 进行注册。
平台设备实验说明
硬件介绍
本节实验使用到STM32MP1 开发板上的RGB 彩灯
硬件原理图分析
参考”字符设备驱动–点亮LED 灯”章节
实验代码讲解
本章的示例代码目录为linux_driver/platform_driver
本节将会把平台设备驱动应用到LED 字符设备驱动的代码中实现硬件与软件代码相分离巩固平台设备驱动的学习。
编程思路
编写第一个内核模块led_pdev.c在内核模块中定义一个平台设备并填充RGB 灯相关设备信息在该模块入口函数注册/挂载这个平台设备编写第二个内核模块led_pdrv.c在内核模块中定义一个平台驱动在probe 函数中完成字符设备驱动的创建在该模块入口函数注册/挂载这个平台驱动
在平台设备总线上注册/挂载平台设备和平台驱动时会自动进行配对。配对成功后回调执行平台驱动的probe 函数从而完成字符设备驱动的创建。
代码分析
定义平台设备
我们需要将字符设备中的硬件信息提取出来独立成一份代码将其作为平台设备注册到内核中。点亮LED 灯需要控制与LED 灯相关的寄存器包括GPIO 时钟寄存器IO 配置寄存器IO数据寄存器等这里的资源实际上就是寄存器地址可以使用IORESOURCE_MEM 进行处理除了这些之外还需要提供一些寄存器的偏移量我们可以利用平台设备的私有数据进行管理。
列表19: 寄存器宏定义(位于…/linux_driver/platform_driver/led_pdev.c)
#define AHB4_PERIPH_BASE (0x50000000)#define RCC_BASE (AHB4_PERIPH_BASE 0x0000) // 时钟控制寄存器
#define RCC_MP_GPIOENA (RCC_BASE 0XA28) // GPIO 时钟使能寄存器#define GPIOA_BASE (AHB4_PERIPH_BASE 0x2000) // GPIOA 外设基地址
#define GPIOA_MODER (GPIOA_BASE 0x0000) // 模式寄存器
#define GPIOA_OTYPER (GPIOA_BASE 0x0004) // 输出类型寄存器
#define GPIOA_OSPEEDR (GPIOA_BASE 0x0008) // 输出速度寄存器
#define GPIOA_PUPDR (GPIOA_BASE 0x000C) // 上下拉寄存器
#define GPIOA_BSRR (GPIOA_BASE 0x0018) // 置位寄存器#define GPIOG_BASE (AHB4_PERIPH_BASE 0x8000)
#define GPIOG_MODER (GPIOG_BASE 0x0000)
#define GPIOG_OTYPER (GPIOG_BASE 0x0004)
#define GPIOG_OSPEEDR (GPIOG_BASE 0x0008)
#define GPIOG_PUPDR (GPIOG_BASE 0x000C)
#define GPIOG_BSRR (GPIOG_BASE 0x0018)#define GPIOB_BASE (AHB4_PERIPH_BASE 0x3000)
#define GPIOB_MODER (GPIOB_BASE 0x0000)
#define GPIOB_OTYPER (GPIOB_BASE 0x0004)
#define GPIOB_OSPEEDR (GPIOB_BASE 0x0008)
#define GPIOB_PUPDR (GPIOB_BASE 0x000C)
#define GPIOB_BSRR (GPIOB_BASE 0x0018)使用宏定义来对GPIO 引脚的寄存器进行封装具体每个寄存器的作用可以参考《STM32MP157》用户手册。
定义一个resource 结构体用于存放上述的寄存器地址提供给驱动使用如下所示
static struct resource rled_resource[] {[0] DEFINE_RES_MEM(GPIOA_MODER, 4),[1] DEFINE_RES_MEM(GPIOA_OTYPER, 4),[2] DEFINE_RES_MEM(GPIOA_OSPEEDR, 4),[3] DEFINE_RES_MEM(GPIOA_PUPDR, 4),[4] DEFINE_RES_MEM(GPIOA_BSRR, 4),[5] DEFINE_RES_MEM(RCC_MP_GPIOENA, 4),
};在内核源码/include/linux/ioport.h 中提供了宏定义DEFINE_RES_MEM、DEFINE_RES_IO、DEFINE_RES_IRQ 和DEFINE_RES_DMA 用来定义所需要的资源类型。DEFINE_RES_MEM 用于定义IORESOURCE_MEM 类型的资源我们只需要传入两个参数一个是寄存器地址另一个是大小。从手册上看可以得知一个寄存器都是32 位的因此这里我们选择需要4 个字节大小的空间。rled_resource 资源数组中我们将所有的MEM 资源进行了编号0 对应了GPIOA_MODER1 对应了GPIOA_OTYPER驱动到时候就可以根据这些编号获得对应的寄存器地址。
列表21: 定义平台设备的私有数据(位于…/linux_driver/platform_driver/led_pdev.c)
unsigned int rled_hwinfo[2] { 13, 0 };使用一个数组rled_hwinfo来记录寄存器的偏移量填充平台私有数据时只需要把数组的首地址赋给platform_data 即可。
关于设备的硬件信息我们已经全部完成了接下来只需要定义一个platform_device 类型的变量填充相关信息。
列表22: 定义平台设备(位于…/linux_driver/platform_driver/led_pdev.c)
static int led_cdev_release(struct inode *inode, struct file *filp)
{return 0;
}/* red led device */static struct platform_device rled_pdev {.name led_pdev,.id 0,1.num_resources ARRAY_SIZE(rled_resource),.resource rled_resource,.dev {.release led_release,.platform_data rled_hwinfo,},};第1-4 行声明了led_cdev_release 函数目的为了防止卸载模块内核提示报错。第7-9 行定义了一个设备名为“led_pdev”的设备这里的名字确保要和驱动的名称保持一致否则就会导致匹配失败。id 编号设置为0驱动会利用该编号来注册设备。第10-11 行将上面实现好的rled_resource 数组赋值给resource 成员同时我们还需要指定资源的数量内核提供了宏定义ARRAY_SIZE用于计算数组长度因此num_resources直接赋值为ARRAY_SIZE(rled_resource)。第12-15 行对dev 中的成员进行赋值将rled_hwinfo 存储到platform_data 中。
最后只需要在模块加载的函数中调用platform_device_register 函数这样当加载该内核模块时新的平台设备就会被注册到内核中去实现方式如下
列表23: 模块初始化(位于…/linux_driver/platform_driver/led_pdev.c)
static __init int led_pdev_init(void)
{printk(pdev init\n);platform_device_register(rled_pdev);return 0;}
module_init(led_pdev_init);static __exit void led_pdev_exit(void)
{printk(pdev exit\n);platform_device_unregister(rled_pdev);}
module_exit(led_pdev_exit);MODULE_AUTHOR(Embedfire);
MODULE_LICENSE(GPL);
MODULE_DESCRIPTION(the example for platform driver);第1-8 行实现模块的入口函数打印信息并注册平台设备第10-16 行实现模块的出口函数打印信息并注销设备第18-20 行模块遵守协议以及一些模块信息
这样我们就实现了一个新的设备只需要在开发板上加载该模块平台总线下就会挂载我们LED 灯的平台设备。
定义平台驱动
我们已经注册了一个新的平台设备驱动只需要提取该设备提供的资源并提供相应的操作方式即可。这里我们仍然采用字符设备来控制我们的LED 灯想必大家对于LED 灯字符设备的代码已经很熟悉了对于这块的代码就不做详细介绍了让我们把重点放在平台驱动上。
我们驱动提供id_table 的方式来匹配设备。我们定义一个platform_device_id 类型的变量led_pdev_ids说明驱动支持哪些设备这里我们只支持一个设备名称为led_pdev要与平台设备提供的名称保持一致。
列表24: id_table(位于…/linux_driver/platform_driver/led_pdrv.c)
static struct platform_device_id led_pdev_ids[] {{.name led_pdev},{}
};
MODULE_DEVICE_TABLE(platform, led_pdev_ids);这块代码提供了驱动支持哪些设备
这仅仅完成了第一个内容这是总线进行匹配时所需要的内容。而在匹配成功之后驱动需要去提取设备的资源这部分工作都是在probe 函数中完成。由于我们采用字符设备的框架因此在probe 过程还需要完成字符设备的注册等工作具体实现的代码如下
列表25: led_pdrv_probe 函数(位于…/linux_driver/platform_driver/led_pdrv.c)
struct led_data {unsigned int led_pin;unsigned int clk_regshift;unsigned int __iomem *va_MODER;unsigned int __iomem *va_OTYPER;unsigned int __iomem *va_OSPEEDR;unsigned int __iomem *va_PUPDR;unsigned int __iomem *va_BSRR;struct cdev led_cdev;};static void __iomem *va_clkaddr;/* 省略部分代码*/static int led_pdrv_probe(struct platform_device *pdev)
{struct led_data *cur_led;unsigned int *led_hwinfo;struct resource *mem_MODER;struct resource *mem_OTYPER;struct resource *mem_OSPEEDR;struct resource *mem_PUPDR;struct resource *mem_BSRR;struct resource *mem_CLK;dev_t cur_dev;int ret 0;printk(led platform driver probe\n);//第一步提取平台设备提供的资源//devm_kzalloc 函数申请cur_led 和led_hwinfo 结构体内存大小cur_led devm_kzalloc(pdev-dev, sizeof(struct led_data), GFP_KERNEL);if(!cur_led)return -ENOMEM;led_hwinfo devm_kzalloc(pdev-dev, sizeof(unsigned int)*2, GFP_KERNEL);if(!led_hwinfo)return -ENOMEM;/* get the pin for led and the regs shift *///dev_get_platdata 函数获取私有数据得到LED 灯的寄存器偏移量并赋值给cur_led-led_pin 和cur_led-clk_regshiftled_hwinfo dev_get_platdata(pdev-dev);cur_led-led_pin led_hwinfo[0];cur_led-clk_regshift led_hwinfo[1];/* get platform resource *///利用函数platform_get_resource 可以获取到各个寄存器的地址mem_MODER platform_get_resource(pdev, IORESOURCE_MEM, 0);mem_OTYPER platform_get_resource(pdev, IORESOURCE_MEM, 1);mem_OSPEEDR platform_get_resource(pdev, IORESOURCE_MEM, 2);mem_PUPDR platform_get_resource(pdev, IORESOURCE_MEM, 3);mem_BSRR platform_get_resource(pdev, IORESOURCE_MEM, 4);mem_CLK platform_get_resource(pdev, IORESOURCE_MEM, 5);//使用devm_ioremap 将获取到的寄存器地址转化为虚拟地址cur_led-va_MODER devm_ioremap(pdev-dev, mem_MODER-start, resource_size(mem_MODER));cur_led-va_OTYPER devm_ioremap(pdev-dev, mem_OTYPER-start,␣resource_size(mem_OTYPER));cur_led-va_OSPEEDR devm_ioremap(pdev-dev, mem_OSPEEDR-start, resource_size(mem_OSPEEDR));cur_led-va_BSRR devm_ioremap(pdev-dev, mem_BSRR-start, resource_size(mem_BSRR));cur_led-va_PUPDR devm_ioremap(pdev-dev, mem_PUPDR-start, resource_ize(mem_PUPDR));va_clkaddr devm_ioremap(pdev-dev, mem_CLK-start, resource_size(mem_CLK));//第二步注册字符设备cur_dev MKDEV(DEV_MAJOR, pdev-id);register_chrdev_region(cur_dev, 1, led_cdev);cdev_init(cur_led-led_cdev, led_cdev_fops);ret cdev_add(cur_led-led_cdev, cur_dev, 1);if(ret 0){printk(fail to add cdev\n);goto add_err;}device_create(my_led_class, NULL, cur_dev, NULL, DEV_NAME %d, pdev-id);/* save as drvdata *///platform_set_drvdata 函数将LED 数据信息存入在平台驱动结构体中pdev-dev-driver_data 中platform_set_drvdata(pdev, cur_led);return 0;add_err:unregister_chrdev_region(cur_dev, 1);return ret;
}第1-15 行仍然使用结构体led_data 来管理我们LED 灯的硬件信息定义时钟寄存器的虚拟地址变量。第39-42 行使用devm_kzalloc 函数申请cur_led 和led_hwinfo 结构体内存大小。第48-51 行使用dev_get_platdata 函数获取私有数据得到LED 灯的寄存器偏移量并赋值给cur_led-led_pin 和cur_led-clk_regshift。第54-59 行利用函数platform_get_resource 可以获取到各个寄存器的地址。第62-67 行在内核中这些地址并不能够直接使用使用devm_ioremap 将获取到的寄存器地址转化为虚拟地址到这里我们就完成了提取资源的工作了。第70-83 行就需要注册一个LED 字符设备了。开发板上板载了三个LED 灯在rled_pdev结构体中我们指定了红灯的ID 号为0我们可以利用该id 号来作为字符设备的次设备号用于区分不同的LED 灯。使用MKDEV 宏定义来创建一个设备编号再调用register_chrdev_region、cdev_init、cdev_add 等函数来注册字符设备。第87 行使用platform_set_drvdata 函数将LED 数据信息存入在平台驱动结构体中pdev-dev-driver_data 中。
当驱动的内核模块被卸载时我们需要将注册的驱动注销相应的字符设备也同样需要注销具体的实现代码如下
列表26: led_pdrv_remove 函数(位于…/linux_driver/platform_driver/led_pdrv.c)
static int led_pdrv_remove(struct platform_device *pdev)
{dev_t cur_dev;struct led_data *cur_data platform_get_drvdata(pdev);printk(led platform driver remove\n);cur_dev MKDEV(DEV_MAJOR, pdev-id);cdev_del(cur_data-led_cdev);device_destroy(my_led_class, cur_dev);unregister_chrdev_region(cur_dev, 1);return 0;}第4 行在probe 函数中调用了platform_set_drvdata将当前的LED 灯数据结构体保存到pdev 的driver_data 成员中在这里调用platform_get_drvdata获取当前LED 灯对应的结构体该结构体中包含了字符设备。第8-11 行调用cdev_del 删除对应的字符设备删除/dev 目录下的设备则调用函数device_destroy最后使用函数unregister_chrdev_region注销掉当前的字符设备编号。
关于操作LED 灯字符设备的方式实现方式如下这里只做简单介绍具体介绍可以参阅LED灯字符设备章节的内容。
列表27: led 灯的字符设备框架(位于…/linux_driver/platform_driver/led_pdrv.c)
static int led_cdev_open(struct inode *inode, struct file *filp)
{
printk(%s\n, __func__);struct led_data *cur_led container_of(inode-i_cdev, struct led_data, led_cdev);unsigned int val 0;// 开启对应的gpio 时钟val readl(va_clkaddr);val | (0x1 cur_led-clk_regshift);writel(val, va_clkaddr);// 设置模式寄存器输出模式val readl(cur_led-va_MODER);val ~((unsigned int)0X3 (2 * cur_led-led_pin));val | ((unsigned int)0X1 (2 * cur_led-led_pin));writel(val,cur_led-va_MODER);// 设置输出类型寄存器推挽模式val readl(cur_led-va_OTYPER);val ~((unsigned int)0X1 cur_led-led_pin);writel(val, cur_led-va_OTYPER);// 设置输出速度寄存器高速val readl(cur_led-va_OSPEEDR);val ~((unsigned int)0X3 (2 * cur_led-led_pin));val | ((unsigned int)0x2 (2 * cur_led-led_pin));writel(val, cur_led-va_OSPEEDR);// 设置上下拉寄存器上拉val readl(cur_led-va_PUPDR);val ~((unsigned int)0X3 (2*cur_led-led_pin));val | ((unsigned int)0x1 (2*cur_led-led_pin));writel(val,cur_led-va_PUPDR);// 设置置位寄存器默认输出高电平val readl(cur_led-va_BSRR);val | ((unsigned int)0x1 (cur_led-led_pin));writel(val, cur_led-va_BSRR);filp-private_data cur_led;return 0;
}static int led_cdev_release(struct inode *inode, struct file *filp)
{return 0;
}static ssize_t led_cdev_write(struct file *filp, const char __user * buf,
size_t count, loff_t * ppos)
{unsigned long val 0;unsigned long ret 0;int tmp count;struct led_data *cur_led (struct led_data *)filp-private_data;kstrtoul_from_user(buf, tmp, 10, ret);// 开启对应的gpio 时钟val readl(va_clkaddr);val | (0x1 cur_led-clk_regshift);writel(val, va_clkaddr);// 设置置位寄存器val readl(cur_led-va_BSRR);if (ret 0)val | ((unsigned int)0x1 ((cur_led-led_pin)16));elseval | ((unsigned int)0x1 (cur_led-led_pin));writel(val, cur_led-va_BSRR);*ppos tmp;return tmp;
}static struct file_operations led_cdev_fops {.open led_cdev_open,.release led_cdev_release,.write led_cdev_write,};第1-40 行是led_cdev_open 函数的内容主要就是对硬件进行初始化。第42-45 行的led_cdev_release 函数的作用是为了防止警告产生。第47-74 行对GPIO 引脚进行置位控制第76-81 行对file_operations 结构体进行填充
最后我们只需要将我们实现好的内容填充到platform_driver 类型的结构体并使用platform_driver_register 函数注册即可。
列表28: 注册平台驱动(位于…/linux_driver/platform_driver/led_pdrv.c)
static struct platform_driver led_pdrv {
.probe led_pdrv_probe,
.remove led_pdrv_remove,
.driver.name led_pdev,
.id_table led_pdev_ids,
};static __init int led_pdrv_init(void)
{printk(led platform driver init\n);my_led_class class_create(THIS_MODULE, my_leds);platform_driver_register(led_pdrv);return 0;
}
module_init(led_pdrv_init);static __exit void led_pdrv_exit(void)
{printk(led platform driver exit\n);platform_driver_unregister(led_pdrv);class_destroy(my_led_class);
}
module_exit(led_pdrv_exit);MODULE_AUTHOR(Embedfire);
MODULE_LICENSE(GPL);
MODULE_DESCRIPTION(the example for platform driver);第1-6 行在led_pdrv 中定义了两种匹配模式在平台总线匹配过程中只会根据id_table中的name 值进行匹配若和平台设备的name 值相等则表示匹配成功反之则匹配不成功表明当前内核没有该驱动能够支持的设备。第8-18 行调用函数class_create来创建一个led 类并且调用函数platform_driver_register注册我们的平台驱动结构体这样当加载该内核模块时就会有新的平台驱动加入到内核中。第20-27 行注销函数led_pdrv_exit则是初始化函数的逆过程。
实验准备
在板卡上的部分GPIO 可能会被系统占用在使用前请根据需要修改/boot/uEnv.txt 文件可注释掉某些设备树插件的加载重启系统释放相应的GPIO 引脚。
如本节实验中可能在鲁班猫系统中默认使能了LED 的设备功能用在了LED 子系统。引脚被占用后设备树可能无法再加载或驱动中无法再申请对应的资源。
方法参考如下 取消LED 设备树插件以释放系统对应LED 资源操作如下 如若运行代码时出现“Device or resource busy”或者运行代码卡死等等现象请按上述情况检查并按上述步骤操作。
如出现Permission denied 或类似字样请注意用户权限大部分操作硬件外设的功能几乎都需要root 用户权限简单的解决方案是在执行语句前加入sudo 或以root 用户运行程序。
编译驱动程序
makefile 修改说明
本节实验使用的Makefile 如下所示编写该Makefile 时只需要根据实际情况修改变量KERNEL_DIR 和obj-m 即可。
列表29: Makefile(位于…/linux_driver/platform_driver/Makefile)
KERNEL_DIR../ebf_linux_kernel/build_image/buildARCHarm
CROSS_COMPILEarm-linux-gnueabihf-
export ARCH CROSS_COMPILEobj-m : led_pdev.o led_pdrv.oall:$(MAKE) -C $(KERNEL_DIR) M$(CURDIR) modules
modules clean:$(MAKE) -C $(KERNEL_DIR) M$(CURDIR) clean编译命令说明
在实验目录下输入如下命令来编译驱动模块
make编译成功后实验目录下会生成两个名为“led_pdev.ko”、”led_pdrv.ko”的驱动模块文件
编译应用程序
本节实验使用linux 系统自带的”echo”应用程序进行测试无需额外编译应用程序
拷贝驱动程序到共享文件夹
此节根据实际情况调整
# 在Ubuntu 上执行
# 拷贝驱动程序、应用程序到共享文件夹中
cp *.ko /home/Embedfire/workdir
# 在开发板上执行
# 将共享文件夹挂载到开发板上将192.168.0.35:/home/Embedfire/workdir 的共享文件夹
路径替换成自己的
sudo mount -t nfs 192.168.0.35:/home/Embedfire/workdir /mnt
# 将共享文件中的.ok 文件拷贝到自己的板子目录下防止加载出错这里直接拷贝到/home 目录下
cp /mnt/*.ko /home程序运行结果
开发板加载第一个驱动模块
教程中为了节省篇幅只列举了一个led 灯配套的例程中提供了三个LED 的代码。当我们运行命令insmod led_pdev.ko 后可以在/sys/bus/platform/devices 下看到我们注册的LED 灯设备共有三个后面的数字0、1、2 对应了平台设备结构体的id 编号。 开发板加载第二个驱动模块
执行命令insmod led_pdrv.ko加载LED 的平台驱动。在运行命令“dmesg|tail”来查看内核打印信息可以看到打印了三次probe分别对应了三个LED 灯设备。 开发板运行应用程序
通过驱动代码最后会在/dev 下创建三个LED 灯设备分别为led0、led1、led2可以使用echo命令来测试我们的LED 驱动是否正常。以红灯/dev/led0为例我们使用命令echo 0 /dev/led0 可控制红灯亮命令echo 1 /dev/led0 可控制红灯亮。 参考资料嵌入式Linux 驱动开发实战指南-基于STM32MP1 系列