- 开发环境:迅为3568开发板 + ubuntu18.04
- 任务:给LED写一个驱动程序,要求当应用程序写入驱动的数据为’1’时点亮LED;当应用程序写入数据为‘0’时熄灭LED。
步骤一:确定控制引脚
打开原理图确定LED的GPIO引脚位置,通过下图知GPIO0_B7可以控制LED9。
GPIO0_B7通过一个三极管控制LED9:
步骤二:配置寄存器
一般情况下,对GPIO 进行配置需要对 GPIO 的复用寄存器,方向寄存器,数据寄存器进行配置:
复用寄存器
通过筛选原理图知GPIO0_B7的复用寄存器在PMU_GRF_GPIO0B_IOMUX_H
上,所以偏移地址为 0x000C
。gpio0b7 可以通过控制[14:12]位来选择复用为哪个功能,要控制led 灯,所以功能要复用为 gpio,即[14:12]的三位全部为0即可。
查找手册知复用寄存器的基地址PMU_GRF
为0xFDC20000
,因此PMU_GRF_GPIO0B_IOMUX_H
的地址为0xFDC20000(基地址)+0x000C(偏移地址)=0xFDC2000C
.
使用io -r -4 0xFDC2000C
可查看寄存器的默认值,通过下图可知,寄存器的 默认值为0x0000001
,也就是说 PMU_GRF_GPIO0B_IOMUX_H
的[14:12]的三位全部为0,因此配置GOIO时可以不用再配置复用寄存器。
方向寄存器
GPIO 共有四组 GPIO,分别是 GPIOA,GPIOB,GPIOC,GPIOD,每组又以 A0~A7, B0~B7, C0~C7, D0~D7 作为编号区分。GPIO_SWPORT_DDR_L 与GPIO_SWPORT_DDR_H上各有两组GPIO,其中GPIO_SWPORT_DDR_L 上可控制GPIOA与GPIOB,GPIO_SWPORT_DDR_H 上可控制GPIOC与GPIOD。
GPIO0B_7 在 GPIO_SWPORT_DDR_L 上,通过下图可知GPIO_SWPORT_DDR_L寄存器地址的计算方式为基地址(0xfDD60000)+偏移地址(0x0008) = 0xFDD60008
。
GPIO的基地址:
通过寄存器的描述可知:寄存器的高16为是控制位,控制低16位数据是否能够写入,二者也是一一对应的关系,如第16为控制第0位,第17为控制第1位,……第31为控制第15位。
因此当我们需要将GPIOB7设置成输出模式时,GPIO_SWPORT_DDR_L寄存器的第15位值为1,第31位值为1。同理,通过命令 io -r -4 0xFDD60008
结果如下,显然符合要求,因此方向寄存器也不用配置。
数据寄存器
GPIO 有四组 GPIO,分别是 GPIOA,GPIOB,GPIOC,GPIOD。每组又以 A0~A7, B0~B7, C0~C7, D0~D7 作为编号区分。GPIO0B7 在 GPIO_SWPORT_DR_L 上。这里的情况类似于方向寄存器GPIO_SWPORT_DDR_L,因此不做过多论述。
数据寄存器GPIO_SWPORT_DR_L 的地址为基地址(0xFDD60000)+偏移地址(0x0000)=0xFDD60000
。
再次通过命令 io -r -4 0xFDD60000
查看当前寄存器的值,结果如下:
由于点亮LED时寄存器的第15位值为1,熄灭LED时第15位值为0,而不管是控制LED熄灭还是点亮寄存器的第31位值一定要是1,因此有:
也可直接使用使用io命令控制检验上述逻辑是否正确:
io -w -4 0xfdd60000 0x80004040
io -w -4 0xfdd60000 0x8000c040
注意:
为了不影响开发板上其他功能,以此配置复用寄存器 方向寄存器 数据寄存器时,先使用io命令查看开发板当前寄存器中的值,再改变需要的某几位值。
可能大家会好奇,为什么要这样。举个栗子,开发板启动需要寄存器A的第1位值为1,而我们点亮LED灯时仅需要第2位值为1,那么如果我们仅仅将寄存器A的值设置成0x0000 0010 那么开发板能够正常工作嘛?显然不能。
由于在linux中不能直接使用物理地址,因此加载驱动时使用地址映射,将物理地址转换成虚拟地址:
// 地址映射
vir_gpio_dr=ioremap(GPIO_DR,4);
if(IS_ERR(vir_gpio_dr))
{
printk("ioremap is failed.\r\n");
iounmap(vir_gpio_dr);
return PTR_ERR(vir_gpio_dr);
}
地址映射函数ioremap,其函数原型为:
#define ioremap(offset, size) \
__ioremap_mode((offset), (size), _CACHE_UNCACHED)
参数释义:
应用程序控制LED灯的实质就是使用write函数写一个控制命令到内核,因此内核只需要判断写入数据是否符合要求即可。
// 开灯
if(kbuff[0] == '1')
{
printk("open led.\r\n");
*vir_gpio_dr = 0x8000c040;
}
// 关灯
else if(kbuff[0] == '0')
{
printk("close led.\r\n");
*vir_gpio_dr = 0x80004040;
}
完整的驱动程序:
#include <linux/kernel.h>
#include <linux/init.h> //初始化头文件
#include <linux/module.h> //最基本的文件,支持动态添加和卸载模块。
#include <linux/miscdevice.h> //注册杂项设备头文件
#include <linux/fs.h> //注册设备节点的文件结构体
#include <linux/uaccess.h>
#include <linux/errno.h>
#include <linux/io.h>
/*
LED设备驱动:需要查看三个寄存器:
复用关系寄存器(PMU_GRF Register) :基地址(0xfdc20000) + 偏移地址(0x000c)
方向寄存器 (GPIO_SWPORT_DDR):基地址(0xFDD60000)+偏移地址((0x0008)
GPIO
| | | |
A B C D
| …… |
0~7 0~7
数据寄存器: 基地址(0xFDD60000)+偏移地址(0x0000)
亮- 0x8000c040 灭-0x80004040
GPIO0的基地址 : 0xFDD60000
*/
// 数据寄存器地址
#define GPIO_DR 0xFDD60000
// 保存虚拟地址
unsigned int *vir_gpio_dr;
// 打开设备
int misc_open(struct inode *inode,struct file*file)
{
printk("misc_open is ok.\r\n");
return 0;
}
// 关闭设备
int misc_close(struct inode * inode, struct file *file)
{
printk("misc_close\r\n");
return 0;
}
// 读取设备中信息
ssize_t misc_read (struct file *file, char __user *buff, size_t size, loff_t *loff)
{
char kbuff[5];
if(copy_to_user(buff,kbuff,strlen(kbuff)) != 0) // 将内核中的数据给应用
{
printk("copy_to_user error\r\n");
return -1;
}
printk("copy_to_user is successful\r\n");
return size;
}
// 写入设备信息
ssize_t misc_write (struct file *file, const char __user *buff, size_t size, loff_t *loff)
{
char kbuff[32] ;
int ret = copy_from_user(kbuff,buff,size);
if( ret != 0) // 从应用那儿获取数据
{
printk("ret of error is %d.\r\n",ret);
return ret;
}
printk("copy_from_user data:%s.\r\n",kbuff);
// 开灯
if(kbuff[0] == '1')
{
printk("open led.\r\n");
*vir_gpio_dr = 0x8000c040;
}
// 关灯
else if(kbuff[0] == '0')
{
printk("close led.\r\n");
*vir_gpio_dr = 0x80004040;
}
return size;
}
// 设备文件操作集
struct file_operations misc_fops ={
.owner = THIS_MODULE,
.open = misc_open,
.release = misc_close,
.read = misc_read,
.write = misc_write
};
// 设备文件信息描述集
struct miscdevice misc_dev = {
.minor = MISC_DYNAMIC_MINOR,
.name = "myLed",
.fops = &misc_fops
};
// 驱动的入口函数
static int __init misc_init(void)
{
// 申请杂项设备
int ret = 0;
ret = misc_register(&misc_dev);
printk("--------------------------------------------\r\n");
if(ret < 0)
printk("misc register is error.\r\n");
else
printk("misc register is seccussful.\r\n");
// 地址映射
vir_gpio_dr=ioremap(GPIO_DR,4);
if(IS_ERR(vir_gpio_dr))
{
printk("ioremap is failed.\r\n");
iounmap(vir_gpio_dr);
return PTR_ERR(vir_gpio_dr);
}
return 0;
}
//驱动的出口函数
static void __exit misc_exit(void)
{
// 注销杂项设备
misc_deregister(&misc_dev);
// 注销地址映射
iounmap(vir_gpio_dr);
printk("baibai\r\n");
}
module_init(misc_init);
module_exit(misc_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("zxj");
编译完成后,使用insmod加载驱动到内核后就可直接编写应用程序检验驱动是否正确。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
if(argc != 2)
{
printf("the number of your input is eror\n");
return -1;
}
char filePath[] = "/dev/myLed";
int fd = open(filePath,O_RDWR);
if(fd < 0)
{
printf("opening is failed.\n");
return -1;
}
else
printf("opening is successful.\n");
write(fd,argv[1],sizeof(argv[1]));
printf("write:%s.\r\n",argv[1]);
close(fd);
return 0;
}
在使用交叉编译并且传输到开发板后,就可运行检验驱动了。检验命令为:
./test 1
就可点亮LED; ./test 0
就可熄灭LED;其实本文的驱动还可以使用字符设备的驱动来编写,但是相对于字符设备驱动,杂项设备驱动更为简单,因此本文采用杂项设备来编写。
如果大家单纯地想看杂项设备与字符设备驱动的编写方式,可点击查阅本文【linux驱动开发】在linux内核中注册一个杂项设备与字符设备以及内核传参的详细教程