LED驱动
其实在本人的理解看来,在驱动上面操控LED,和使用STM32在操控LED是大同小异的,因为本质都是控制引脚的输出电平,来达到点亮或者熄灭LED的作用,在这里,我们想要操控LED,我们首先要先清除它的原理图是什么样的。
如上图所示可知,想要点亮LED,那么我们需要控制引脚,使其输出低电平,方可打开LED,输出高电平,关闭LED。同时由图可知,LED对应的引脚在GPIO5_3,下面我们来详细分析操控LED的步骤。
LED驱动的操控步骤
如果我们想要操控LED,那么我们首先需要打开它对应GPIO的时钟,然后,由于芯片上面的很多引脚一般被设置了很多功能,有可能是UART、IIC等,我们需要设置我们需要的GPUO引脚为GPIO功能。设置引脚为GPIO功能后,我们需要设置引脚为输出/输入模式,现在我们想要操控LED灯,那么我们需要设置其为输出模式。此时,LED就已经初始化完成了,我们只需要控制对应寄存器,就可以控制LED对应引脚输出高电平或者低电平了。
引脚对应寄存器
想要实现如上步骤,那么我们需要通过查阅芯片手册,了解到对应引脚的寄存器,以及对应的物理地址(如果不知道引脚对应的物理地址,是无法操控寄存器的),如下,我们需要使用到总共四个寄存器。
第一步我们先查找CCM,也就是打开总线时钟的寄存器的基地址,LED的引脚为GPIO5_3,那么我们直接在文档里面搜索GPIO5,可以找到如下
然后直接搜索 CCGR1 寄存器,就可以找到对应的打开时钟的功能寄存器。
如图我们可知,我们需要把这个32位的寄存器的第 30-31 位设置为1,才可以打开时钟,同时我们也可以知道这个寄存器的地址,地址是一定要使用的,否则我们无法操控。
第二步我们寻找操控引脚复用的寄存器,首先我们搜索GPIO5,得到如下
因为我们是GPIO5的引脚3,所以我们找到如上信息,再次进行搜索,我们可以得到如下两个寄存器:
SW_MUX_CTL_PAD_SNVS_TAMPER3 SW MUX Control Register
SW_PAD_CTL_PAD_SNVS_TAMPER3 SW PAD Control Register
SW_MUX_CTL_PAD_SNVS_TAMPER3 这个寄存器才是用来配置引脚复用功能的,我们仅需找到它的地址就好。
可知设置低四位为 0101 时,表示为GPIO功能
第三步我们需要设置GPIO的引脚为输出功能,同样的查看芯片手册,搜索GPIO5
此寄存器的全称为,GPIO5的方向寄存器,那么就是设置它为输出或者输入了,点击跳转到此寄存器的详细界面
地址为基地址加上0x4,也就是0x20AC000加上0x4
可知,设置为1表示输出模式,我们需要设置bit3为1
最后就是我们设置引脚输出高电平或者低电平的寄存器了,文档中搜索GPIO5
当设置bit3为1,则输出高电平,反之则输出低电平
寄存器名称 | 寄存器地址 | 寄存器功能 |
CCM_CCGR1 | 0x20C406C | 打开时钟 |
SW_MUX_CTL_PAD_SNVS_TAMPER3 | 0x2290014 | 引脚复用 |
GPIO5_GDIR | 0x20AC004 | 设置为输出模式 |
GPIO5_DR | 0x20AC000 | 设置输出电平高低 |
具体代码实现
我们需要先设置四个指针,稍后用来存储物理地址映射过来的虚拟地址
(为什么这里要用到volatile,是为了防止编译器对变量进行优化)
/* 使能时钟 */
static volatile unsigned int* CCM_CCGR1 = NULL;
/* 引脚复用 */
static volatile unsigned int* IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = NULL;
/* 设置为输出模式 */
static volatile unsigned int* GPIO5_GDIR = NULL;
/* 设置输出高电平/低电平 */
static volatile unsigned int* GPIO5_DR = NULL;
然后,我们进行映射,把物理地址通过MMU映射到虚拟地址上,在这里我们使用ioremap这个函数,就是为了映射物理地址到虚拟地址上,好让我们可以操控这些寄存器。需要注意的是,ioremap映射的单元并不是以字节算的,而是以页表算的,也就是4096字节,如下填入4也就是映射4个页表的字节
CCM_CCGR1 = ioremap(0x20C406C , 4 );
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x2290014 , 4 );
GPIO5_GDIR = ioremap(0x020AC004, 4);
GPIO5_DR = ioremap(0x020AC000, 4);
那么具体的操作就如下所示
CCM_CCGR1用来开启时钟,如上我们可知,我们需要设置寄存器的bit 30-31 为1 ,那么 (3 << 30)也就是二进制 0011 左移三十位
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3因为需要设置的bit包含0,那么为了防止之前寄存器已经被设置过,我们需要首先对需要设置的那几个bit清零,然后在进行设置,(|=5 , 由于低四位此时为0000,当|=5也就是0101后,低四位变为0101,此时bit0-3为101)
GPIO5_GDIR ,设置bit3为1,表示设置为输出模式
*CCM_CCGR1 |= (3 << 30);
/* 因为引脚复用为GPIO为0101,包含0,所以先清零,防止原本的位包含1,导致|1后,还会是1 */
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 &= ~(0xf);
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 |= 5;
*GPIO5_GDIR |= (1 << 3);
经过如上三个寄存器的设置之后,此时只需要设置GPIO5_DR寄存器就可以控制引脚的输入输出了,控制此引脚的bit3,置0表示输出低电平,反之输出高电平
/* 输出低电平 */
*GPIO5_DR &= ~(1 << 3);
/* 输出高电平 */
*GPIO5_DR |= (1 << 3);
完整代码实现
完整的代码分为 应用层程序、驱动程序、Makefile
应用层程序
通过open打开设备节点,然后对设备节点进行操作来达到点灯和熄灯的目的
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char** argv)
{
char status = 0;
if( argc != 3 )
{
printf("./ledtest /dev/myled on \n");
printf("./ledtest /dev/myled off \n");
return -1;
}
int fd;
//open
fd = open(argv[1] , O_RDWR);
if( fd < 0 )
{
printf("open %s file \n",argv[1]);
return -1;
}
//write
if( 0 == strcmp(argv[2] , "on") )
{
status = 1;
write(fd , &status , 1);
}
else
{
status = 0;
write(fd , &status , 1);
}
return 1;
}
驱动层程序
其实GPIO5的时钟默认是打开的,所以我在这里没有在对此寄存器进行操作
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/io.h>
/********************** 无法卸载驱动,要先释放设备device在释放class ***************************/
/*
如果要使用到物理设备的话,需要把外设的地址映射到虚拟地址上
LED的流程
1、使能
2、设置引脚为GPIO
3、设置为输出模式
4、设置值
*/
/*
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 地址:0x02290000 + 0x14
设置引脚复用
*/
static volatile unsigned int* IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
/*
GPIO5_GDIR 地址:0x020AC004
设置引脚输出模式
*/
static volatile unsigned int* GPIO5_GDIR;
/*
GPIO5_DR 地址:0x020AC000
设置引脚输出高电平/低电平
*/
static volatile unsigned int* GPIO5_DR;
//主设备号
static int major;
//节点
static struct class *led_class;
ssize_t led_write (struct file *file, const char __user *buf, size_t size, loff_t *ppos)
{
/* 判断应用层想要写入的数据 */
int ret;
char val;
ret = copy_from_user(&val, buf, 1);
if(val)
{
/* open led */
*GPIO5_DR &= ~(1 << 3);
}
else
{
/* off led */
*GPIO5_DR |= (1 << 3);
}
return 1;
}
int led_open (struct inode *inode, struct file *file)
{
/* 设置引脚服用为GPIO */
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 &= ~0xf;
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 |= 0x5;
/* 设置为输出模式 */
*GPIO5_GDIR |= (1 << 3);
return 0;
}
static const struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.write = led_write,
};
/* 入口函数 */
static int __init led_init(void)
{
printk("%s %s %d \n",__FILE__,__FUNCTION__,__LINE__);
/* 注册结构体到内核 */
major = register_chrdev(0, "myled", &led_fops);
/* 映射物理地址到虚拟地址上 */
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x02290000 + 0x14 , 4);
GPIO5_GDIR = ioremap(0x020AC004, 4);
GPIO5_DR = ioremap(0x020AC000, 4);
/* 创建节点 */
led_class = class_create(THIS_MODULE, "myled");
/* 创建设备 */
device_create(led_class, NULL, MKDEV(major, 0), NULL, "myled");
return 0;
}
/* 有注册函数就有卸载函数 */
static void __exit led_exit(void)
{
/* 清除物理地址的映射 */
iounmap(IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3);
iounmap(GPIO5_GDIR);
iounmap(GPIO5_DR);
/* 设备卸载 */
device_destroy(led_class, MKDEV(major, 0));
/* 卸载节点 */
class_destroy(led_class);
/* 卸载结构体从内核 */
unregister_chrdev( major, "myled");
}
/* 需要用某些宏表示上述两个函数分别是入口函数和出口函数 */
module_init(led_init);
module_exit(led_exit);
/* 遵循GPL协议 */
MODULE_LICENSE("GPL");
Makefile
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
all:
make -C $(KERN_DIR) M=`pwd` modules
$(CROSS_COMPILE)gcc -o led_driver_test led_driver_test.c
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
rm -f led_driver_test
obj-m += led_driver.o