在没有设备树的情况下,需要手动注册 platform 设备,下面分别注册platform 设备 和 platform 驱动,以此来驱动LED。
一、platform 设备注册
新建文件 led-device.c 文件,该文件保存的是外设信息,在当前背景下便是 LED 相关的寄存器信息,该文件最终会被编程成 ko 模块文件,加入到 Linux 内核中。
1、注册 / 注销 platform 设备
在 Linux 内核中使用 platform_device 类型来表示 platform 设备,platform_device 类型的实例化对象为 leddevice,重点是 name、num_resources、resource 成员的初始化:
- name:platform设备名,用于和下面 platform 驱动进行匹配
- release:dev 成员下的 release 函数,当platform 设备从内核移除时调用
- num_resources:resource 数组的资源个数(数组中元素个数)
- resource:资源数组,每一个元素都对应一个寄存器的相关信息
static struct platform_device leddevice = {
.name = "imx6ull-led",
.id = -1,
// .dev = {
// .release = led_release,
// },
.num_resources = ARRAY_SIZE(led_resources),
.resource = led_resources,
};
static int __init leddevice_init(void)
{
return platform_device_register(&leddevice);
}
static void __exit leddevice_exit(void)
{
platform_device_unregister(&leddevice);
}
module_init(leddevice_init);
module_exit(leddevice_exit);
2、resource 数组定义
现定义一个 resource 类型的数组,数组名为 led_resources,数组中的每一个元素都对应一个寄存器。resource 类型数组成员的解析放在上一篇。
#define CCM_CCGR1_BASE 0x20C406C // 时钟源
#define IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03_BASE 0x20E0068 // IO 复用
#define IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03_BASE 0x20E02F4 // 复用引脚初始化
#define GPIO1_GDIR_BASE 0x209C004 // 输出方向
#define GPIO1_DR_BASE 0x209C000 // LED输出引脚
#define REGISTER_LENGTH 0x04 // 寄存器地址长度
static struct resource led_resources[] = {
[0] = {
.start = CCM_CCGR1_BASE,
.end = CCM_CCGR1_BASE + REGISTER_LENGTH - 1,
.flags = IORESOURCE_MEM
},
[1] = {
.start = IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03_BASE,
.end = IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03_BASE + REGISTER_LENGTH - 1,
.flags = IORESOURCE_MEM
},
[2] = {
.start = IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03_BASE,
.end = IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03_BASE + REGISTER_LENGTH - 1,
.flags = IORESOURCE_MEM
},
[3] = {
.start = GPIO1_GDIR_BASE,
.end = GPIO1_GDIR_BASE + REGISTER_LENGTH - 1,
.flags = IORESOURCE_MEM
},
[4] = {
.start = GPIO1_DR_BASE,
.end = GPIO1_DR_BASE + REGISTER_LENGTH - 1,
.flags = IORESOURCE_MEM
},
};
3、完整 platform 设备注册
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/uaccess.h>
#include <linux/cdev.h> // cdev_init
#include <linux/device.h> // device_create
#include <linux/err.h> // IS_ERR
#include <asm/io.h> // ioremap、iounmap
#include <linux/platform_device.h>
/* 寄存器物理地址 */
#define CCM_CCGR1_BASE 0x20C406C // 时钟源
#define IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03_BASE 0x20E0068 // IO 复用
#define IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03_BASE 0x20E02F4 // 复用引脚初始化
#define GPIO1_GDIR_BASE 0x209C004 // 输出方向
#define GPIO1_DR_BASE 0x209C000 // LED输出引脚
#define REGISTER_LENGTH 0x04 // 寄存器地址长度
static struct resource led_resources[] = {
[0] = {
.start = CCM_CCGR1_BASE,
.end = CCM_CCGR1_BASE + REGISTER_LENGTH - 1,
.flags = IORESOURCE_MEM
},
[1] = {
.start = IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03_BASE,
.end = IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03_BASE + REGISTER_LENGTH - 1,
.flags = IORESOURCE_MEM
},
[2] = {
.start = IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03_BASE,
.end = IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03_BASE + REGISTER_LENGTH - 1,
.flags = IORESOURCE_MEM
},
[3] = {
.start = GPIO1_GDIR_BASE,
.end = GPIO1_GDIR_BASE + REGISTER_LENGTH - 1,
.flags = IORESOURCE_MEM
},
[4] = {
.start = GPIO1_DR_BASE,
.end = GPIO1_DR_BASE + REGISTER_LENGTH - 1,
.flags = IORESOURCE_MEM
},
};
void led_release(struct device *dev)
{
printk("led device released!\n");
}
static struct platform_device leddevice = {
.name = "imx6ull-led",
.id = -1,
// .dev = {
// .release = led_release,
// },
.num_resources = ARRAY_SIZE(led_resources),
.resource = led_resources,
};
static int __init leddevice_init(void)
{
return platform_device_register(&leddevice);
}
static void __exit leddevice_exit(void)
{
platform_device_unregister(&leddevice);
}
module_init(leddevice_init);
module_exit(leddevice_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("author_name");
二、platform 驱动注册
新建文件 led-driver.c 文件,该文件提供了上层应用层所需的驱动接口,如 close 函数对应驱动中的release 接口,将在当前文件中实现。该文件最终也会被编程成 ko 模块文件,加入到 Linux 内核中。这里的逻辑基本最初学习的驱动逻辑几乎相同,如注册设备号、注册设备节点、实现 file_operations 中的文件接口。
详情可参考:字符设备驱动模板
1、注册 / 注销 platform 驱动
在 Linux 内核中使用 platform_driver 类型来表示 platform 设备,当前背景下 platform_driver 类型的实例化对象为 led_driver
- name:platform 驱动名称,用于和上面 platform 设备进行匹配
- probe:platform 驱动和platform 设备匹配成功时调用,该函数中可以添加之前的 注册设备号、注册设备节点 等操作。
- remove:platform 驱动从内核移除时调用,该函数中可以添加之前的 注销字符设备 等操作。
/* 驱动结构体 */
static struct platform_driver led_driver = {
.driver = {
.name = "imx6ull-led",
},
.probe = led_probe,
.remove = led_remove,
};
static int __init leddriver_init(void)
{
printk("platled driver init!\n");
// 注册 platform 驱动
return platform_driver_register(&led_driver);
}
static void __exit leddriver_exit(void)
{
printk("platled driver removed!\n");
// 注销 platform 驱动
platform_driver_unregister(&led_driver);
}
module_init(leddriver_init);
module_exit(leddriver_exit);
2、加载 platform 设备资源
既然添加了 platform 设备,那就要想办法从 platform 设备中获取我们需要的寄存器信息。使用的接口为 platform_get_resource,接口原型如下:
/**
* @param pdev 要访问的platform设备
* @param type 资源类型,对应resource结构体中的 flag 成员
* @param index 访问resource数组的下标
* @return 成功返回 resource 数组的首地址,失败返回0
*/
struct resource *platform_get_resource(struct platform_device * pdev,
unsigned int type,
unsigned int index);
设备和驱动匹配成功后,probe 函数的第一个参数会传递 platform_device 指针,这个可以作为 platform_get_resource 的第一个参数。具体获取方式如下:
static int led_probe(struct platform_device *dev)
{
uint32_t i = 0;
uint32_t ret = 0;
uint32_t ressize[5]; // 保存每个寄存器地址大小
struct resource* ledresource[5]; // 获取 platform 设备中的resource数组
/* 获取资源 */
for (i = 0; i < 5; i++)
{
ledresource[i] = platform_get_resource(dev, IORESOURCE_MEM, i);
if (!ledresource[i])
{
printk("fetch resource failed\n");
return -1;
}
ressize[i] = resource_size(ledresource[i]);
}
/* 建立物理地址和虚拟地址的映射 */
CCM_CCGR1 = ioremap(ledresource[0]->start, ressize[0]);
IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = ioremap(ledresource[1]->start, ressize[1]);
IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 = ioremap(ledresource[2]->start, ressize[2]);
GPIO1_GDIR = ioremap(ledresource[3]->start, ressize[3]);
GPIO1_DR = ioremap(ledresource[4]->start, ressize[4]);
// ... ...
}
到此便已经和最初的 LED 驱动对接上了,重复的代码这里就不再赘述,下面提供一份完整的
3、完整 platform 驱动注册
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/uaccess.h>
#include <linux/cdev.h> // cdev_init
#include <linux/device.h> // device_create
#include <linux/err.h> // IS_ERR
#include <asm/io.h> // ioremap、iounmap
#include <linux/platform_device.h>
#define CHRDEVBASE_NAME "platled" /* 设备名 */
enum LED_STAT {
LED_ON,
LED_OFF
};
struct chrdev_led_t{
struct class* class; /* 设备节点所属类 */
struct device* device; /* 设备节点 */
struct cdev dev; /* 字符设备 */
dev_t devid; /* 设备号 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
/* 寄存器虚拟地址 */
static void __iomem* CCM_CCGR1;
static void __iomem* IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03;
static void __iomem* IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03;
static void __iomem* GPIO1_GDIR;
static void __iomem* GPIO1_DR;
static u32 val;
static struct chrdev_led_t chrdev_led;
static void led_on(void)
{
val = readl(GPIO1_DR);
val &= ~(1 << 3);
writel(val, GPIO1_DR);
}
static void led_off(void)
{
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
}
/*
* @description : 打开设备
* @param – pinode : 传递给驱动的 inode
* @param - pfile : 设备文件,file 结构体有个叫做 private_data 的成员变量
* 一般在 open 的时候将 private_data 指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int chrdevbase_open(struct inode *pinode, struct file *pfile)
{
/* 用户实现具体功能 */
printk("open platled driver\n");
pfile->private_data = &chrdev_led;
return 0;
}
/*
* @description : 向设备写数据
* @param - pfile : 要打开的设备文件(文件描述符)
* @param - buf : 要给设备写入的数据(用户缓冲区)
* @param - cnt : 要写入的数据长度
* @param - offset : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t chrdevbase_write(struct file *pfile, const char __user *buf, size_t cnt, loff_t *offset)
{
// 获取模块数据
struct chrdev_led_t* pdev = pfile->private_data;
unsigned char databuf[1];
unsigned char ledstat;
// 将数据从用户缓冲区拷贝到内核缓冲区
int ret = copy_from_user(databuf, buf, cnt);
if(ret != 0)
return 0;
ledstat = buf[0] - '0';
printk("led state: %d\n", ledstat);
if (ledstat == LED_ON)
{
led_on();
}
else if(ledstat == LED_OFF)
{
led_off();
}
return cnt;
return 0;
}
/*
* @description : 关闭/释放设备
* @param - pfile : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int chrdevbase_release (struct inode *pinode, struct file * pfile)
{
/* 用户实现具体功能 */
printk("close chrdevbase\n");
return 0;
}
/*
* 设备操作函数结构体
*/
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
static int led_probe(struct platform_device *dev)
{
uint32_t i = 0;
uint32_t ret = 0;
uint32_t ressize[5];
struct resource* ledresource[5];
/* 获取资源 */
for (i = 0; i < 5; i++)
{
ledresource[i] = platform_get_resource(dev, IORESOURCE_MEM, i);
if (!ledresource[i])
{
printk("fetch resource failed\n");
return -1;
}
ressize[i] = resource_size(ledresource[i]);
}
/* 建立物理地址和虚拟地址的映射 */
CCM_CCGR1 = ioremap(ledresource[0]->start, ressize[0]);
IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = ioremap(ledresource[1]->start, ressize[1]);
IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 = ioremap(ledresource[2]->start, ressize[2]);
GPIO1_GDIR = ioremap(ledresource[3]->start, ressize[3]);
GPIO1_DR = ioremap(ledresource[4]->start, ressize[4]);
/* GPIO1 时钟源初始化 */
val = readl(CCM_CCGR1);
val |= (3 << 26);
writel(val, CCM_CCGR1);
/* IO复用 */
writel(5, IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03);
/* IO复用引脚初始化 */
writel(0x10B0, IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03);
/* GPIO1_IO03 输出使能 */
val = readl(GPIO1_GDIR);
val |= (1 << 3);
writel(val, GPIO1_GDIR);
/* LED 熄灭(GPIO1的第3个引脚为高电平) */
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
/* 1. 注册设备号 */
if (chrdev_led.major)
{
chrdev_led.devid = MKDEV(chrdev_led.major, 0);
ret = register_chrdev_region(chrdev_led.devid, 1, CHRDEVBASE_NAME);
}
else
{
ret = alloc_chrdev_region(&chrdev_led.devid, 0, 1, CHRDEVBASE_NAME);
chrdev_led.major = MAJOR(chrdev_led.devid);
chrdev_led.minor = MINOR(chrdev_led.devid);
}
/* 2. 初始化字符设备 */
chrdev_led.dev.owner = THIS_MODULE;
cdev_init(&chrdev_led.dev, &chrdevbase_fops); // 初始化字符设备
/* 3. 将字符设备添加到内核 */
cdev_add(&chrdev_led.dev, chrdev_led.devid, 1); // 将字符设备添加到内核
/* 自动创建设备节点 */
// 设备节点所属类
chrdev_led.class = class_create(THIS_MODULE, CHRDEVBASE_NAME);
if (IS_ERR(chrdev_led.class))
{
return PTR_ERR(chrdev_led.class);
}
// 创建设备节点
chrdev_led.device = device_create(chrdev_led.class, NULL, chrdev_led.devid, NULL, CHRDEVBASE_NAME);
if (IS_ERR(chrdev_led.device))
{
return PTR_ERR(chrdev_led.device);
}
printk("chrdevbase init!\n");
return 0;
}
static int led_remove(struct platform_device *dev)
{
/* 取消虚拟地址和物理地址的映射 */
iounmap(CCM_CCGR1);
iounmap(IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03);
iounmap(IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03);
iounmap(GPIO1_GDIR);
iounmap(GPIO1_DR);
/* 注销字符设备 */
unregister_chrdev_region(chrdev_led.devid, 1); // 注销设备号
cdev_del(&chrdev_led.dev); // 卸载字符设备
device_destroy(chrdev_led.class, chrdev_led.devid); // 删除节点
class_destroy(chrdev_led.class); // 删除类
return 0;
}
/* 驱动结构体 */
static struct platform_driver led_driver = {
.driver = {
.name = "imx6ull-led",
},
.probe = led_probe,
.remove = led_remove,
};
static int __init leddriver_init(void)
{
printk("platled driver init!\n");
return platform_driver_register(&led_driver);
}
static void __exit leddriver_exit(void)
{
printk("platled driver removed!\n");
platform_driver_unregister(&led_driver);
}
module_init(leddriver_init);
module_exit(leddriver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("author_name");
三、Makefile 编译
Makefile 的编译也和之前有所不同,这里提供一份编译多个模块的 Makefile 脚本,MODULE_NAME 变量是创建的两个 c 文件名,main.c 是测试程序,这个就参考之前的了,生成的执行文件为 testApp 。
ARCH := arm
CROSS_COMPILE := arm-linux-gnueabihf-
CC := $(CROSS_COMPILE)gcc
KERNEL_DIR := /home/gzx/IMX6ULL/kernel/4.1.15/linux-imx-4.1.15-source-compiled
CURRENR_DIR := $(shell pwd)
MODULE_NAME := led-device led-driver
obj-m += $(foreach subitem,${MODULE_NAME},$(subitem).o)
kofile = $(patsubst %.o,%.ko,${obj-m})
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNEL_DIR) M=$(CURRENR_DIR) modules
$(CC) -o testApp main.c
.PHONY:clean
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURRENR_DIR) clean
@rm testApp