GPIO是最简单的硬件操作,GPIO输出又更易于GPIO输入,所以驱动的学习也应始于GPIO输出操作。废话不多说了,现在就来写一个简单的LED字符设备驱动,用来点亮和熄灭板子上的LED灯。
我用的板子是正点原子的ALPHA,使用的芯片是IMX6ULL,板子上有一颗LED灯,使用的IO口是GPIO1_IO03,我们通过字符设备控制这个IO口输出高电平和低电平,从而达到控制LED灯的亮灭。
我写的也不是什么教程,我也是跟着别人的教程在学习,所以就不罗嗦什么原理之类的了,就直接贴代码了。不过要说明的是,代码是我自己优化过的,因为经过了自己的思考,所以绝对会比教程里面的代码更合理,也更贴近实际工程中使用,大家通过与教程代码对比也可以看得出来。
驱动源码工程下载链接:一个最基本的LED字符设备驱动
leddrv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/cdev.h>
#include <linux/ide.h>
/*这个宏用于获取寄存器位掩码中有效位的偏移位置
val: 寄存器的位掩码值,1表示有效位(连续的),0表示无效位
pos: 获取到的偏移位置,为右侧0(无效位)的个数
*/
#define GET_MASK_OFFSET(val, pos) \
for(pos = 0; pos < 32; pos++) \
if(val & (0x1 << pos)) break; \
#define LED_ON (0)
/*配置一个GPIO引脚所需要使用的的寄存器集合,该GPIO引脚在寄存器中的对应配置位以及配置值
[0]: 寄存器地址
[1]: 有效配置位
[2]: 配置值
*/
struct gpio_regs
{
unsigned int ccm_reg[3];
unsigned int mux_reg[3];
unsigned int pad_reg[3];
unsigned int gdir_reg[3];
unsigned int dr_reg[3];
};
/*寄存器的内存映射地址集合,打开一个LED时可作为该LED的私有数据
*/
struct led_resource
{
void __iomem *ccm_map;
void __iomem *mux_map;
void __iomem *pad_map;
void __iomem *gdir_map;
void __iomem *dr_map;
unsigned int bitmask; //记录DR寄存器的控制位,用于后续的电平输出
};
/*该数组中gpio_regs结构体的个数决定了LED灯的数量,一个gpio_regs结构体代表一个LED灯
*/
static const struct gpio_regs led_regs[] = {
{
{ 0x020C406C, 0x0C000000, 0x3 }, //GPIO时钟模块配置
{ 0x020E0068, 0x0000000F, 0x5 }, //GPIO引脚复用配置
{ 0x020E02F4, 0x0001FFFF, 0x10B0 }, //GPIO引脚电气属性配置
{ 0x0209C004, 0x00000008, 0x1 }, //GPIO引脚配置为输出
{ 0x0209C000, 0x00000008, 0x1 }, //GPIO默认输出高电平
},
//可在此添加gpio_regs结构体,增加LED灯
};
static int led_count;
static int led_major;
static struct class *leddrv_class;
static struct led_resource *led_res;
static int leddrv_open(struct inode *inode, struct file *filp)
{
unsigned int value, offset;
int minor = iminor(inode);
//设备号超过LED数量
if(minor >= led_count)
return -1;
/*在OPEN里面初始化具体的硬件,因为打开一个设备才表示真正要用到这个设备*/
//开启GPIO模块时钟
GET_MASK_OFFSET(led_regs[minor].ccm_reg[1], offset);
value = readl(led_res[minor].ccm_map);
value &= (~led_regs[minor].ccm_reg[1]);
value |= (led_regs[minor].ccm_reg[2] << offset);
writel(value, led_res[minor].ccm_map);
//复用引脚为GPIO
GET_MASK_OFFSET(led_regs[minor].mux_reg[1], offset);
value = readl(led_res[minor].mux_map);
value &= (~led_regs[minor].mux_reg[1]);
value |= (led_regs[minor].mux_reg[2] << offset);
writel(value, led_res[minor].mux_map);
//设置引脚电气属性
GET_MASK_OFFSET(led_regs[minor].pad_reg[1], offset);
value = readl(led_res[minor].pad_map);
value &= (~led_regs[minor].pad_reg[1]);
value |= (led_regs[minor].pad_reg[2] << offset);
writel(value, led_res[minor].pad_map);
//配置GPIO引脚为输出
GET_MASK_OFFSET(led_regs[minor].gdir_reg[1], offset);
value = readl(led_res[minor].gdir_map);
value &= (~led_regs[minor].gdir_reg[1]);
value |= (led_regs[minor].gdir_reg[2] << offset);
writel(value, led_res[minor].gdir_map);
//默认输出高电平
GET_MASK_OFFSET(led_regs[minor].dr_reg[1], offset);
value = readl(led_res[minor].dr_map);
value &= (~led_regs[minor].dr_reg[1]);
value |= (led_regs[minor].dr_reg[2] << offset);
writel(value, led_res[minor].dr_map);
filp->private_data = (void*)&led_res[minor];
return 0;
}
static ssize_t leddrv_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
int ret;
char val;
unsigned value, offset;
struct led_resource *res;
int minor = iminor(file_inode(filp));
//设备号超过LED数量
if(minor >= led_count)
return -1;
//获取LED设备对应的资源结构体
res = (struct led_resource *)(filp->private_data);
if(!res)
return -1;
//获取用户参数
ret = copy_from_user(&val, buf, 1);
if(ret < 0)
return -EFAULT;
//设置GPIO引脚电平
GET_MASK_OFFSET(res->bitmask, offset);
value = readl(res->dr_map);
value &= (~res->bitmask);
if(val == LED_ON)
value &= (0x0 << offset);
else
value |= (0x1 << offset);
writel(value, res->dr_map);
return 1;
}
static int leddrv_close(struct inode *inode, struct file *filp)
{
struct led_resource *res;
unsigned value, offset;
int minor = iminor(inode);
//设备号超过LED数量
if(minor >= led_count)
return -1;
//获取LED设备对应的资源结构体
res = (struct led_resource *)(filp->private_data);
if(!res)
return -1;
//关闭LED灯
GET_MASK_OFFSET(res->bitmask, offset);
value = readl(res->dr_map);
value &= (~res->bitmask);
value |= (0x1 << offset);
writel(value, res->dr_map);
filp->private_data = NULL;
return 0;
}
static const struct file_operations drv_fops = {
.owner = THIS_MODULE,
.open = leddrv_open,
.write = leddrv_write,
.release = leddrv_close,
};
static int __init leddrv_init(void)
{
dev_t dev;
int i, ret;
//获取LED数量
led_count = sizeof(led_regs) / sizeof(struct gpio_regs);
if(led_count < 1) return 0;
//分配LED内存映射结构体
led_res = (struct led_resource *)kzalloc(sizeof(struct led_resource) * led_count, GFP_KERNEL);
if(!led_res) {
ret = -ENOMEM;
goto err_maps_memory;
}
//将LED用到的寄存器进行内存映射
for(i = 0; i < led_count; i++) {
led_res[i].ccm_map = ioremap(led_regs[i].ccm_reg[0], 4);
led_res[i].mux_map = ioremap(led_regs[i].mux_reg[0], 4);
led_res[i].pad_map = ioremap(led_regs[i].pad_reg[0], 4);
led_res[i].gdir_map = ioremap(led_regs[i].gdir_reg[0], 4);
led_res[i].dr_map = ioremap(led_regs[i].dr_reg[0], 4);
led_res[i].bitmask = led_regs[i].dr_reg[1];
}
//注冊字符设备驱动
led_major = register_chrdev(0, "led driver", &drv_fops);
if(led_major < 0) {
printk(KERN_ERR "Led driver - cannot register device\n");
ret = led_major;
goto err_register_dev;
}
//为LED设备创建设备节点
leddrv_class = class_create(THIS_MODULE, "led class");
if(IS_ERR(leddrv_class)) {
printk(KERN_ERR "Led driver - cannot create class\n");
ret = PTR_ERR(leddrv_class);
goto err_create_class;
}
for(i = 0; i < led_count; i++) {
dev = MKDEV(led_major, i);
device_create(leddrv_class, NULL, dev, NULL, "alpha-led-%d", i);
}
return 0;
err_create_class:
unregister_chrdev(led_major, "led driver");
err_register_dev:
for(i = 0; i < led_count; i++) {
iounmap(led_res[i].ccm_map);
iounmap(led_res[i].mux_map);
iounmap(led_res[i].pad_map);
iounmap(led_res[i].gdir_map);
iounmap(led_res[i].dr_map);
}
kfree(led_res);
err_maps_memory:
return ret;
}
static void __exit leddrv_exit(void)
{
int i;
dev_t dev;
for(i = 0; i < led_count; i++) {
dev = MKDEV(led_major, i);
device_destroy(leddrv_class, dev);
}
class_destroy(leddrv_class);
unregister_chrdev(led_major, "led driver");
for(i = 0; i < led_count; i++) {
iounmap(led_res[i].ccm_map);
iounmap(led_res[i].mux_map);
iounmap(led_res[i].pad_map);
iounmap(led_res[i].gdir_map);
iounmap(led_res[i].dr_map);
}
kfree(led_res);
}
module_init(leddrv_init);
module_exit(leddrv_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("David.Tang");
这个驱动程序只有一个leddrv.c文件,仅仅使用了几个Linux内核提供的字符设备驱动接口函数,没有使用任何框架,从字符设备的注册到操作具体的GPIO,全都在一个leddrv.c文件里面,简单直观。
我们能找到的大多数教程都是将LED的个数以及要使用的寄存器地址用宏定义的方式写死。在实际使用中,这非常不便于扩展,而且让人感觉很奇怪,因为“写死”在编程中就是不灵活不可扩展的代称。我这里进行了改善,定义一个struct gpio_regs
结构体来表示LED灯要用到的寄存器以及配置值。这样一个struct gpio_regs
结构体就代表了一个LED灯,有多少个LED灯就定义多少个结构体就行了,这就是面向对象的思维,非常重要。
这是一个简单粗暴的LED字符设备驱动,简单直观,但是还存在问题,我们下一篇文章接着进行优化。
学习心得:
1)Linux驱动 = 驱动框架 + 单片机(硬件操作),我们要花更多的精力在学习Linux驱动框架上,理解和复用已有的框架,可以加快开发的速度和兼容别人的应用。
2)在驱动的open函数里面才配置具体的硬件,而不是在驱动的入口函数里面。因为硬件往往可以复用为多个功能,加载一个驱动也并不代表要使用一个硬件设备,只有open一个设备的时候才表示要使用那个硬件。