注:本文的参考文档《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.3.pdf》,只用于学习记录。
1. 实验环境
硬件平台:Jz2440 开发板
linux内核:linux-4.15
2. Linux 下 LED 灯驱动原理
Linux 下的任何外设驱动,最终都是要配置相应的硬件寄存器。所以LED驱动最终也要落实对IO相关寄存器的配置,与裸机不同的是,Linux下编写驱动需要符合Linux的驱动框架。
2.1 查看原理图
对LED进行操作我们需要知道LED接到SOC的哪一个IO上,是高电平点亮LED还是低电平点亮LED,JZ2440开发板的LED部分的原理图下图所示:
从LED的原理图可知:
(1) LED D10、D11、D12 IO输出低电平点亮、输出高电平熄灭;
(2) LED D10、D11、D12 连接SOC的IO分别是GPF4、GPF5、GPF6;
2.2 查看SOC芯片手册配置IO寄存器
S3C2440芯片手册GPF的相关寄存器信息如下:
从上图可知:
(1) 我们需要配置的寄存器有GPFCON、GPFDAT;
(2) GPFCON、GPFDAT寄存器对应的物理地址分别是0x56000050、0x56000054;
(3) 把GPF4~GPF6配置为输出,需要把GPFCON寄存器的bit[8:13] = 010101;
(4) 可以通过对GPFDAT的 bit[4:6] 写值来控制GPF4~GPF6的输出状态;
2.3 地址映射
老版本Linux中要求处理器有MMU,但是现在Linux 内核已经支持无 MMU 的处理器了,MMU的主要功能如下:
(1) 完成虚拟空间到物理空间的映射;
(2) 内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。
注:Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚拟 地 址 。
在linux系统中,要完成物理内存和虚拟内存之间的转换,设计到两个函数:ioremap 和 iounmap。
2.3.1 ioremap函数
ioremap 函数用于获取指定物理地址空间对应的虚拟地址空间,定义在arch/arm/include/asm/io.h 文件中,定义如下:
void __iomem *ioremap(resource_size_t res_cookie, size_t size);
#define ioremap ioremap
/*ioremap的实现在arch/arm/mm/ioremap.c*/
void __iomem *ioremap(resource_size_t res_cookie, size_t size)
{
return arch_ioremap_caller(res_cookie, size, MT_DEVICE,
__builtin_return_address(0));
}
ioremap 是一个宏,也是一个函数名,它由两参数 有两个参数和一个返回值,它们的含义如下:
res_cookie:要映射给的物理起始地址,在include/linux/types.h 有定义:typedef phys_addr_t resource_size_t;
size:要映射的内存空间大小;
返回值:__iomem 类型的指针,指向映射后的虚拟空间首地址,对于32位的处理器,映射的内存长度为 4字节;
2.3.2 iounmap函数
卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射, iounmap 函数原型如下:(也在arch/arm/include/asm/io.h 文件)
void iounmap(volatile void __iomem *iomem_cookie);
#define iounmap iounmap
iounmap 只有一个参数 iomem_cookie,此参数就是要取消映射的虚拟地址空间首地址。
2.4 I/O 内存访问函数
这里说的 I/O 是输入/输出的意思,并不是我们学习单片机的时候讲的 GPIO 引脚。这里涉及到两个概念: I/O 端口和 I/O 内存。当外部寄存器或内存映射到 IO 空间时,称为 I/O 端口。当外部寄存器或内存映射到内存空间时,称为 I/O 内存。 使用 ioremap 函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。

2.4.1 读操作函数
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)
readb、 readw 和 readl 这三个函数分别对应 8bit、 16bit 和 32bit 读操作,参数 addr 就是要读取写内存地址,返回值就是读取到的数据。
2.4.2 写操作函数
void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)
writeb、 writew 和 writel 这三个函数分别对应 8bit、 16bit 和 32bit 写操作,参数 value 是要写入的数值, addr 是要写入的地址。
3. LED驱动程序编写
(1) 编写led_drv.c 驱动程序,代码如下:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/types.h>
#include <linux/errno.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <asm/mach/map.h>
#include <asm/io.h>
#define DEVICE_NAME "led"
#define GPF_BASE_ADDR 0x56000050
#define LED_ON 1
#define LED_OFF 0
static struct led_dev_t{
struct cdev cdev;/*定义字符设备*/
int major; /*主设备号*/
struct class *class; /*类*/
struct device *device;/*设备*/
unsigned int __iomem *gpfcon; /*gpfcon 寄存器*/
unsigned int __iomem *gpfdat; /*gpfdat 寄存器*/
}led_dev;
static void led_switch(u32 on_off)
{
u32 val;
if(on_off == 1){
val = readl(led_dev.gpfdat);
val &= ~((1<<4) | (1<<5) | (1<<6));
writel(val,led_dev.gpfdat);
}else{
val = readl(led_dev.gpfdat);
val |= ((1<<4) | (1<<5) | (1<<6));
writel(val,led_dev.gpfdat);
}
}
static int led_open(struct inode *inode, struct file *filp)
{
u32 val;
filp->private_data = &led_dev;
val = readl(led_dev.gpfcon);
val &= ~(3<<(4*2) | 3<<(5*2) | 3<<(6*2));
val |= 1<<(4*2)| 1<<(5*2)| 1<<(6*2);
writel(val, led_dev.gpfcon);
//printk("gpfcon:0x%x\n",*led_dev.gpfcon);
return 0;
}
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
u32 val,ret;
ret = copy_from_user(&val,buf,cnt);
if(ret < 0){
printk("kernel write failed!\r\n");
return -EFAULT;
}
if (val == 1){
led_switch(LED_ON);
}else{
led_switch(LED_OFF);
}
return 0;
}
static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
static int __init led_init(void)
{
dev_t devid;
/*1.创建设备号*/
if(led_dev.major){
devid = MKDEV(led_dev.major,0);
register_chrdev_region(devid, 1, DEVICE_NAME);
}else{
alloc_chrdev_region(&devid, 0, 1, DEVICE_NAME);
led_dev.major = MAJOR(devid);
}
/*2.初始化字符设备cdev*/
cdev_init(&led_dev.cdev, &led_fops);
/*3.向内核添加一个cdev*/
cdev_add(&led_dev.cdev, devid, 1);
/*4.创建类*/
led_dev.class = class_create(THIS_MODULE, DEVICE_NAME);
if(IS_ERR(led_dev.class)){
return PTR_ERR(led_dev.class);
}
/*5.创建设备*/
led_dev.device = device_create(led_dev.class, NULL, devid, NULL, DEVICE_NAME);
if(IS_ERR(led_dev.device)){
return PTR_ERR(led_dev.device);
}
/*6.初始化硬件*/
led_dev.gpfcon = ioremap(GPF_BASE_ADDR, 8);
led_dev.gpfdat = led_dev.gpfcon + 1;
return 0;
}
static void __exit led_exit(void)
{
cdev_del(&led_dev.cdev); /*删除cdev*/
unregister_chrdev_region(MKDEV(led_dev.major,0),1); /*注销设备号*/
device_destroy(led_dev.class, MKDEV(led_dev.major,0)); /*删除设备*/
class_destroy(led_dev.class);
iounmap(led_dev.gpfcon);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
(2) 编写Makefile,代码如下:
KERN_DIR = /home/book/works/linux-4.15
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
(3) 编写测试app,led_app.c 的代码如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
/*int main(int argc,char **argv) 参数详解
* argc:命令行总的参数个数
* argv:对应argc个参数,其中argv[0]是程序的全名,后面的参数对应着用户输入的参数
*
*例如:应用程序:led_app on
* 其中:led_app是应用程序的名称,on是用户输入的参数
* 此时:argc = 2, argv[0] = "led_app" , argv[1] = "on"
*/
int main(int argc,char **argv)
{
int val;
int fd;
fd = open("/dev/led",O_RDWR);
if(fd<0) printf("can't open!\n");
if (argc != 2)
{
printf("Usage :\n");
printf("%s <on|off>\n", argv[0]);
return 0;
}
if(strcmp(argv[1],"on") == 0) val = 1;
else val = 0;
write(fd,&val,sizeof(val));
close(fd);
return 0;
}
测试时输入命令:./led_app on
,点亮LED;输入命令:./led_app off
,熄灭LED。