linux 中LED的驱动
一、相关知识点(涉及接口、结构体、调用关系等)
一个软件系统可以分为以下四层:应用程序、库、内核、驱动,借用韦老师一副图,如下图:
即:应用层开发的功能,各个接口会通过库、通过内核调用到底层驱动程序的对应接口,从而执行对应的功能。
1.1 裸机开发步骤与驱动开发过程对比
1.1.1 裸机开发步骤
裸机开发时,我们需要按照如下过程进行开发。
步骤:
1、看电路图,查清楚led在哪个管脚,对应哪个GPIO口
2、看芯片手册,查看需要配置的相关寄存器地址——(主要查看需要配置哪些寄存器可以将GPIO配置成输出模式,地址可以在map地址映射章节找到)
3、看芯片手册,查清楚怎么样能将GPIO配置成输出高电平或低电平。
4、编译代码,烧写到板卡中,重新上电
1.1.2 Linux系统下LED驱动开发步骤
备注:裸机开发与带操作系统的驱动开发对寄存器的操作不同,裸机开发直接操作寄存器地址即可,而带linux操作系统时,必须要进行地址映射(有相关接口可以直接使用),映射完成后,再进行操作即可。
步骤:
1、撰写基本字符驱动框架代码(字符驱动代码框架实现步骤详见第2节:基本驱动框架实现流程)
2、看电路图,查清楚led在哪个管脚,对应哪个GPIO口
3、看芯片手册,查看需要配置的相关寄存器地址——(主要查看需要配置哪些寄存器可以将GPIO配置成输出模式,地址可以在map地址映射章节找到)
4、在驱动入口函数与出口函数中完成寄存器地址映射与取消映射
5、将led操作功能代码填充到驱动框架中
1.2 预备知识:写驱动时涉及的接口、结构体、宏等
name 含义
struct file_operations 结构体:该结构体中告诉每一个接口成员指向某个函数指针,指向的函数指针则为要实现的功能
struct class 类
module_init(lxxx_init); 注册驱动入口接口
module_exit(xxx_exit); 注册驱动出口接口
MODULE_LICENSE(“Dual BSD/GPL”); 声明
xxx_init() 驱动入口函数
xxx_exit() 驱动出口函数
register_chrdev() 注册模块,告诉内核让内核知道有这个模块,并且返回值为自动分配的主设备号
unregister_chrdev() 卸载驱动程序,告诉内核
class_create() 创建一个类
device_create(major, …); 创建一个设备,使用该接口创建设备以后,在板卡中insmod xxx之后,会自动生成/dev/设备,并且会将major主设备号分配给该设备
device_destroy() 删除设备,删除设备号
class_destroy 删除类
ioremap(); 映射寄存器地址
iounmap(); 删除映射关系
二、基本驱动框架
本小结,作为预备知识,先对字符驱动基本框架做一个简要描述,具体从两个方面进行描述,分别是:
2.1 基本驱动框架实现流程(思路)
2.2 基本驱动框架结构
2.1 基本驱动框架实现流程(思路)
撰写驱动框架时,按照以下驱动思路来写即可,后续代码实现,即可对比参照此流程来分析代码。
步骤如下:
1、实现结构体:【xxx_fops】。该结构体内部制定了相关的功能代码接口,从而应用能够知道哪个对应功能在哪个接口中实现。
2、实现步骤1中的相关函数(读写函数)框架,此时不需要实现函数具体细节,函数内部为空即可。
3、驱动入口函数:【xxx_init()】
xxx_init() {
(a)在函数前面定义全局变量【int major】,下一步用该变量接收系统自动分配的主设备号。
(b)驱动入口函数中实现注册函数,返回值为系统自动分配的主设备号:major = register_chrdev(0,设备名称,结构体);
}
4、驱动入口修饰函数:【modules_init();】
5、驱动出口函数:【xxx_exit();】
xxx_exit() {
(a)驱动出口函数中实现卸载函数:【unregister_chrdev(主设备号,设备名称)】
}
6、驱动出口修饰函数:【modules_exit();】
7、给sysfs文件系统提供更多驱动信息,这样上层应用因为【udev机制】便可自动创建设备节点,跟底层驱动代码中自动生成的主设备号保持一致,从而关联起来。(备注:udev机制个mdev机制基本可以认为是同一个东西,mdev是udev的简化版本)
(a)定义结构体class:【static struct class *xxx_class;】
(b)驱动入口函数xxx_init()中增加两个接口:【创建类class_create()】和【创建设备device_create()】
(c)驱动出口函数xxx_exit()中增加两个接口:【删除设备device_destroy()】和【删除类class_destroy()】
(d)注意:步骤b和步骤c中两个接口顺序是相反的!!
8、添加Licence:【MODULE_LICENSE(“Dual BSD/GPL”);】。
2.2 基本驱动框架结构
/* 头文件 */
#include xxxxxxx
/* 功能接口1 */
x1()
{
...
}
/* 功能接口2 */
x2()
{
...
}
/* 功能接口3 */
x3()
{
...
}
/* 重要结构体:指定对应功能接口,该结构体中告诉每一个接口成员指向某个函数,指向的函数则为要实现的功能 */
static struct file_operations xxx_fops {
.owner = THIS_MODULE, /* 固定格式 */
.其他接口1 = x1, /* 函数指针:指向x1,功能实现接口1 */
.其他接口2 = x2, /* 函数指针:指向x2,功能实现接口2 */
.其他接口3 = x3, /* 函数指针:指向x3,功能实现接口3 */
...
};
/* 驱动入口函数 */
xxx_init()
{
major = register_chrdev(...); /* 注册模块,告诉内核让内核知道有这个模块,并且返回值为自动分配的主设备号 */
class_create(...); /* 创建一个类 */
device_create(major, ...); /* 创建一个设备,使用该接口创建设备以后,在板卡中insmod xxx之后,会自动生成/dev/设备,并且会将major主设备号分配给该设备 */
ioremap(); /* 映射寄存器地址 */
}
/* 驱动出口函数 */
xxx_exit()
{
unregister_chrdev(major, ...); /* 卸载驱动程序,告诉内核 */
device_destroy(xxx_class, MKDEV(major, 0)); /* 删除设备,删除设备号 */
class_destroy(xxx_class); /* 删除类 */
iounmap(); /* 删除映射关系 */
}
module_init(xxx_init);
module_exit(xxx_exit);
MODULE_LICENSE("Dual BSD/GPL");
三、代码实现分析
不管是有系统的驱动代码,还是无系统裸机操作,说到底,最底层始终是对寄存器进行操作,裸机无非是直接操作寄存器,配置好相关寄存器,开始运行程序即可。有linux系统时,区别仅在于,不能直接操作寄存器,需要对寄存器地址进行映射,之后在操作映射后的地址(接口为:remap()和unmap())。
按照第一小节实现思路步骤,与上一章节的基本驱动框架,逐步往下实现
3.1 Led操作驱动框架代码
按照第二小节框架,写出led驱动框架代码如下:
我先计划功能实现如下:使用open、release、write接口实现对GPIO的操作
1、在open函数中实现GPIO寄存器配置为输出功能
2、在release函数中实现GPIO寄存器配置为输出低电平
3、在write函数中实现GPIO寄存器配置为输出高电平
/* 头文件 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
static int major = (-1); /* 初始化为无效值 */
static struct class *led_drv_class;
/* 功能接口1 */
static int led_drv_open(struct inode *inode, struct file *file)
{
/* 功能:配置GPIO为输出功能,待填充 */
return 0;
}
/* 功能接口2 */
static int led_drv_release(struct inode *pinode , struct file *pfile)
{
/* 功能:配置GPIO为低电平,待填充 */
return 0;
}
/* 功能接口3 */
static ssize_t led_drv_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
/* 功能:配置GPIO为高电平,待填充 */
return 0;
}
/* 结构体,对应成员分别指向各个功能接口 */
static struct file_operations led_drv_fops = {
.owner = THIS_MODULE,
.open = led_drv_open, /* 函数指针:指向x1,功能实现接口1 */
.release = led_drv_release, /* 函数指针:指向x2,功能实现接口2 */
.write = led_drv_write, /* 函数指针:指向x3,功能实现接口3 */
};
/* 驱动入口函数 */
static int led_drv_init(void)
{
major = register_chrdev(0, "led_drv", &led_drv_fops); // 注册, 告诉内核,返回值major为自动分配的主设备号
led_drv_class = class_create(THIS_MODULE, "leddrv");
device_create(led_drv_class, NULL, MKDEV(major, 0), NULL, DEV_NAME); /* insmod xxx后,会自动生成/dev/xyz设备,并且自动给该设备分配主设备号major */
/* 映射寄存器地址,待填充 */
return 0;
}
/* 驱动出口函数 */
static void led_drv_exit(void)
{
unregister_chrdev(major, "led_drv"); /* 卸载驱动程序,告诉内核 */
device_destroy(led_drv_class, MKDEV(major, 0));
class_destroy(led_drv_class);
/* 删除映射关系,待填充 */
}
module_init(led_drv_init);
module_exit(led_drv_exit);
MODULE_LICENSE("Dual BSD/GPL");
3.2 看电路图,找到led分别对应哪个GPIO口
打开电路图,我们看到rico board开发板led分别为D23/D24/D25/D26分别对应GPIO1_24/GPIO1_25/GPIO1_26/GPIO1_27
对应关系如下表格:
led名字 对应GPIO
D23 GPIO1_24
D24 GPIO1_25
D25 GPIO1_26
D26 GPIO1_27
3.3 查看芯片手册,查看需要配置那些寄存器,以及各个寄存器地址
看芯片手册第28.3.4.3章节,如下图:
大致意思是:当GPIO作为输出功能,我们需要配置以下三个寄存器从而让对应GPIO口输出高低电平:
寄存器名称 对应功能
GPIO_OE 配置GPIO口功能
GPIO_CLRDATAOUT GPIO输出低电平寄存器
GPIO_SETDATAOUT GPIO输出高电平寄存器
继续往下看手册,跳转到三个对应寄存器的描述:
3.3.1 GPIO_OE
可以看到两个信息:
1、该寄存器偏移地址为:0x134
2、含义:OE寄存器为使能GPIO输出功能,当复位以后,GPIO输入输出功能不可用,再看表格中的Description可知:当该寄存器写入0时,表示GPIO为输出功能,故我们需要将GPIO_OE的24/25/26/27bit配置为0。
3.3.2 GPIO_CLRDATAOUT
1、该寄存器偏移地址为:0x190
2、寄存器对应位写入0时不影响原功能,写入1时清除GPIO口输出寄存器(GPIO_DATAOUT),即GPIO口会输出低电平
3.3.3 GPIO_SETDATAOUT
1、该寄存器偏移地址为:0x194
2、寄存器对应位写入0时不影响原功能,写入1时置位GPIO口输出寄存器(GPIO_DATAOUT),即GPIO口会输出高电平
3.3.4 GPIO1基地址
在文档中搜索GPIO1,Memory Map章节中搜到GPIO1基地址为0x4804C000
3.3.5 宏定义寄存器地址
根据以上资料,为了代码中操作方便,使用宏定义寄存器地址。
/* 寄存器地址宏定义 */
#define GPIO1_BASE_ADDR 0x4804c000
#define GPIO1_OE_ADDR ((GPIO1_BASE_ADDR) + (0x134))
#define GPIO1_DATA_OUT_ADDR ((GPIO1_BASE_ADDR) + (0x13C))
#define GPIO1_CLEAR_DATA_OUT_ADDR ((GPIO1_BASE_ADDR) + (0x190))
#define GPIO1_SET_DATA_OUT_ADDR ((GPIO1_BASE_ADDR) + (0x194))
3.4 完成寄存器地址映射与删除寄存器地址映射
volatile unsigned long *gpio1_oe = NULL;
volatile unsigned long *gpio1_dataout = NULL;
volatile unsigned long *gpio1_clrdata = NULL;
volatile unsigned long *gpio1_setdata = NULL;
/* 映射寄存器地址 */
gpio1_oe = (volatile unsigned long *)ioremap(GPIO1_OE_ADDR, 32);
gpio1_dataout = (volatile unsigned long *)ioremap(GPIO1_DATA_OUT_ADDR, 32);
gpio1_clrdata = (volatile unsigned long *)ioremap(GPIO1_CLEAR_DATA_OUT_ADDR, 32);
gpio1_setdata = (volatile unsigned long *)ioremap(GPIO1_SET_DATA_OUT_ADDR, 32);
/* 删除映射关系 */
iounmap(gpio1_setdata);
iounmap(gpio1_clrdata);
iounmap(gpio1_dataout);
iounmap(gpio1_oe);
3.5 完成led操作功能代码
由于功能简单,此处不再赘述,直接看最后一章节完整代码即可,各个功能都有注释。
3.6 将上述步骤中各个功能代码填充到驱动框架中
可以将上述所有功能代码全部封装成接口,因为功能比较简单,这里我没有做封装,直接将代码填充到驱动框架中,完整代码如下一章节。
四、led驱动代码(完整代码,亲测有效)
备注:附github工程连接:https://github.com/Warrior-Asa/led_drv
4.1 led_drv.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#define DEV_NAME "led"
/* led */
#define GPIO1_BASE_ADDR 0x4804c000
#define GPIO1_OE_ADDR ((GPIO1_BASE_ADDR) + (0x134))
#define GPIO1_DATA_OUT_ADDR ((GPIO1_BASE_ADDR) + (0x13C))
#define GPIO1_CLEAR_DATA_OUT_ADDR ((GPIO1_BASE_ADDR) + (0x190))
#define GPIO1_SET_DATA_OUT_ADDR ((GPIO1_BASE_ADDR) + (0x194))
volatile unsigned long *gpio1_oe = NULL;
volatile unsigned long *gpio1_dataout = NULL;
volatile unsigned long *gpio1_clrdata = NULL;
volatile unsigned long *gpio1_setdata = NULL;
/* led end */
static int major = (-1); /* 初始化为无效值 */
static struct class *led_drv_class;
static int led_drv_open(struct inode *inode, struct file *file)
{
/* 将oe寄存器bit25、26、27置0:表示output功能 */
*gpio1_oe &= ((~(1<<25)) | (~(1<<26)) | (~(1<<27)));
return 0;
}
static int led_drv_release(struct inode *pinode , struct file *pfile)
{
/* 将setdata寄存器bit25、26、27置1,表示输出高电平 */
*gpio1_setdata |= ((1<<25) | (1<<26) | (1<<27));
return 0;
}
static ssize_t led_drv_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
/* 将cleardata寄存器bit25、26、27置1,表示清除输出,则输出低电平 */
*gpio1_clrdata |= ((1<<25) | (1<<26) | (1<<27));
return 0;
}
static struct file_operations led_drv_fops = {
.owner = THIS_MODULE,
.open = led_drv_open,
.release = led_drv_release,
.write = led_drv_write,
};
/* 驱动入口函数 */
static int led_drv_init(void)
{
major = register_chrdev(0, "led_drv", &led_drv_fops); // 注册, 告诉内核,返回值major为自动分配的主设备号
led_drv_class = class_create(THIS_MODULE, "leddrv");
device_create(led_drv_class, NULL, MKDEV(major, 0), NULL, DEV_NAME); /* insmod xxx后,会自动生成/dev/xyz设备,并且自动给该设备分配主设备号major */
/* 映射寄存器地址 */
gpio1_oe = (volatile unsigned long *)ioremap(GPIO1_OE_ADDR, 32);
gpio1_dataout = (volatile unsigned long *)ioremap(GPIO1_DATA_OUT_ADDR, 32);
gpio1_clrdata = (volatile unsigned long *)ioremap(GPIO1_CLEAR_DATA_OUT_ADDR, 32);
gpio1_setdata = (volatile unsigned long *)ioremap(GPIO1_SET_DATA_OUT_ADDR, 32);
return 0;
}
/* 驱动出口函数 */
static void led_drv_exit(void)
{
unregister_chrdev(major, "led_drv"); /* 卸载驱动程序,告诉内核 */
device_destroy(led_drv_class, MKDEV(major, 0));
class_destroy(led_drv_class);
/* 删除映射关系 */
iounmap(gpio1_setdata);
iounmap(gpio1_clrdata);
iounmap(gpio1_dataout);
iounmap(gpio1_oe);
}
module_init(led_drv_init);
module_exit(led_drv_exit);
MODULE_LICENSE("Dual BSD/GPL");
4.2 Makefile
KERN_DIR = ~/mier/Kernel/linux-3.12.10-ti2013.12.01
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += led_drv.o
4.3 编译生成.ko文件(make)
至此代码写完,执行make命令,会生成.ko文件
五、加载/卸载模块等常用操作(开发板命令行常用操作)
将上步骤生成【led_drv.ko】文件拷贝到开发板中。
5.1 加载驱动到内核(insmod xxx.ko)
在开发板中执行命令insmod led_drv.ko,使用lsmod命令查看当前已加载的模块,可以看到led_drv模块已经加载到内核中
5.2 cat /proc/devices查看设备
使用命令cat /proc/devices可以查看该模块分配的设备号为243,左边一列数字为主设备号,右边字符串为设备名称
5.3 命令rmmod xxx
使用命令rm led_drv可以卸载该模块
六、led测试程序【led_app.c】
6.1 led_app.c
现在回到linux开发环境中,写一套led驱动测试app代码,代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define DEV_NAME "/dev/led"
int main(int argc, char const *argv[])
{
int fd = 0;
int ret = 0;
int led_status = 0;
fd = open(DEV_NAME,O_RDWR); /* 根据设备名称打开设备 */
if (fd<0) {
printf("You are wrong!!!\n");
exit(0);
}
ret = write(fd, &led_status, 1); /* 向设备中写入0,灯亮 */
printf("Set led GPIO to %d\n",ret);
sleep(5);
close(fd);
return 0;
}
6.2 编译,生成可执行文件
使用交叉编译工具链进行编译,生成可执行文件led_app
将该可执行文件拷贝到开发板中。
七、测试效果
7.1 先将之前加载进内核的led_drv模块卸载掉,执行./led_app
如下图,因为内核中没有自己写的led_drv驱动模块,所以应用程序打开/dev/led时打开失败,提示you are wrong!
7.2 加载模块,再执行应用程序./led_app
先将自己写的驱动模块加载到内核中,在执行测试程序./led_app,可以看到应用正常执行,设备正常打开,观察开发板,led亮5秒之后熄灭,测试成功
八、github工程连接
github工程连接:https://github.com/Warrior-Asa/led_drv