LED驱动程序

1,344 阅读16分钟

前言

上一篇文章中,我们说了"Linux驱动=软件框架+硬件操作",对于软件框架我们给出了一个大体流程,本篇文章来说说如何进行硬件操作。

正文

类似学习一门新语言,我们都是先写个Hello World程序,而对于驱动程序操作硬件来说,最简单的操作就是点亮LED灯。那么如何点亮LED灯呢?这个步骤涉及较多知识点,大致分为3步:

  1. 查看开发板原理图,结合开发板实际情况,找到要控制的LED灯,看LED是如何连线的,使用哪个引脚来控制它的点亮和熄灭
  2. 确定引脚后,需要查看芯片的开发手册,因为引脚或者叫做GPIO口的使用是由芯片控制的,需要了解想让该引脚输出高低电平来控制LED需要哪些模块配合,比如如何使能引脚,如何设置引脚功能,如何让其输出高低电平等,这一部分工作必须熟悉芯片开发手册。
  3. 写程序控制引脚的输出,如何控制呢?在芯片内部有非常多的寄存器,而引脚的工作就依赖于寄存器的值,所以我们需要了解涉及哪些寄存器,以及设置那些值。

这只是一个大体步骤,每一步都会有很多坑,我们一一来看。

LED简介

什么是LED,它就是一个灯,有电流过的时候能亮,LED样子有多种,比如:

image.png

因为长的不一样,所以需要使用原理图给抽象出来。LED只有2个角,且是分方向,所以用三角形来表示能让其点亮的点亮方向,同时为了保护LED,还需要加个电阻。

同时可以通过编程,把电路串联上主芯片来控制LED的开关,如图:

image.png

接下来我们来看一下在开发板原理图中,LED一般是如何表示的。根据芯片引脚的输出电平能力以及LED的连接方式,常见的有如下几种:

image.png

其中Res表示电阻,VCC为电源,GND为接地,LED加箭头表示LED以及点亮点亮的方向,NPN或者PNP是三极管,根据连接方式有如下结论:

  1. 对于方式1来说,引脚输出3.3V点亮LED,输出0V熄灭LED。

  2. 对于方式2来说,引脚输出3.3V熄灭LED,输出0V点亮LED。

    有的芯片为了省电,或者引脚的驱动能力不足,可能只可以输出1.2V。

  3. 对于方式3来说,当输出1.2V时,NPN三极管可以导通,点亮LED,输出0V时,熄灭LED。

  4. 对于方式4来说,输出0V点亮LED,输出1.2V熄灭LED。

由此可见想点亮LED,与其相连的引脚高电平低电平都可以,主要看开发板原理图。对于有的可以输出3.3V,有的可以输出1.2V,在后续写代码时,我们都认为它是逻辑高电平即可。

GPIO引脚的普适操作

由原理图可以知道,在一般情况下我们只需要设置该LED连接的GPIO引脚是输出高低电平即可,这里先来看一下GPIO引脚的一些普适操作。

GPIO是General-purpose input/output的缩写,即通用的输入输出口。对于一个GPIO模块来说有如下重要信息:

  • 一般有多组GPIO,每组有多个GPIO引脚
  • 使能GPIO组,通过电源模块或者时钟模块对该组GPIO进行使能。
  • 对于某个引脚,可以设置用于GPIO或者其他功能,需要设置引脚的模式,即引脚复用。
  • 设置引脚为GPIO功能时,可以设置它是输入引脚还是输出引脚,即方向
  • 数值:
    • 对于输出引脚,可以设置寄存器让其输出高低电平。
    • 对于输入引脚,可以读取寄存器得到引脚的高低电平。

这几点非常重要,大体说明了如何使用某个GPIO引脚,比如这里的使能GPIO组、设置GPIO复用模式等,都是通过设置芯片的寄存器来完成的。

GPIO寄存器操作

在后面写具体驱动程序时,我们会对寄存器进行操作,但是操作寄存器,有一个基本原则:不能影响到其他位。这里有2种方式:

  1. 直接读写,读出、修改对应位,并且写入。代码如下:
val = data_reg;
val = val | (1<<n);
data_reg = val;

这里的data_reg表示是寄存器,是一个十进制的值,但是一般在芯片手册上表示为是一个bit数组,假如是16位无符号数字,那么值就是从16个0到16个1,每一位可能表示不同的含义。

比如原来的值是1111000011110000,现在需要把从低到高的第10位设置为1,只需要把val或上10 0000 0000(1<<9),这样值就变成了1111001011110000了,这种通过移位再或的方式是配置某一项为1的最简单方式。

当要清除bit n时,代码如下:

val = data_reg;
val = val & ~(1<<n);
data_reg = val;

这也是一种通用的写法,把某个位置为0。比如需要把第6位置为0,那么1<<5就是100000,然后~取反得到011111,最后再和1111000011110000进行&与操作,得到1111000011010000

  1. 通过set-and-clear protocol机制,对于某些芯片来说,为了更方便操作寄存器,会提供set_regclr_reg辅助寄存器,使用起来就非常方便了。

比如要设置bit n,直接set_reg = (1<<n);,不必再读出来/写入的操作了。比如要清除bit n,直接clr_reg = (1<<n)即可让第n位设置为0。

原理图和芯片手册

话不多说,我们直接来根据刚开始说的3步来进行。

查看原理图

首先看一下硬件,我们想操作哪个LED,这里有IMX6ULL板子的硬件图:

image.png

image.png

我们想直接操作序号2即用户LED灯,这时可以打开IMX6ULL器件图,找到左上角,截图如下:

image.png

可以发现该LED名字是LED2,还连了一个R50电阻,这时打开开发板原理图,找到LED2,如下图:

image.png

这就是前面说过的原理图了,可以发现该LED和GPIO5_3这个引脚相连,同时因为VDD_3V3的电压在左边,所以当GPIO5_3输出低电压时,LED点亮;当GPIO5_3输出高电压时,LED熄灭

得到这个结论,起码我们知道了点亮该LED需要输出低电压还是高电压。

查看芯片手册

芯片手册一般都非常多,比如我使用的IMX6ULL有4000多页,还是英文的,所以这东西熟能生巧。首先要知道操作GPIO的大体步骤,这个在前面普适操作中说了,只有这样才可以有目的性的查询芯片手册。其次就是芯片手册中各个章节的作用,要有一个大体的认识,这样才方便更快找到需要用的寄存器。

GPIO章节概述

GPIO通用输入输出外设,提供了专业的、通用的pin脚,这些pin脚可以设置为输入或者输出。

当设置为输出时,可以通过写入内部寄存器来控制引脚上的状态驱动;当设置为输入时,可以通过读内部寄存器来监控引脚上的状态。同时,GPIO外设还可以产生CORE中断。

GPIO的功能实现是通过8个寄存器,一个边沿检测电路和中断产生逻辑构成。这里的8个寄存器如下:

  • Data register,GPIO数据寄存器,简写为GPIO_DR;

  • GPIO direction register,GPIO方向寄存器,简写为GPIO_GDIR;

  • Pad sample register,GPIO引脚采样寄存器,简称为GPIO_PSR。GPIO接口可以配置为输入模式,用于检测外部事件或信号的变化。GPIO_PSR用于获取GPIO引脚的采样值,记录了最近一次对GPIO引脚进行采样时的状态。

    当GPIO引脚被设置为输入模式时,可以通过读取GPIO_PSR寄存器获取当前引脚的采样值,该采样值代表了引脚上的电平状态,可以是高电平或低电平。

  • Interrupt control registers,GPIO中断控制寄存器,简称为GPIO_ICR1和GPIO_ICR2;

  • Edge select register,GPIO边沿选择寄存器,简称为GPIO_EDGE_SEL。GPIO接口可以设置为输入模式,用于检测外部事件或者信号的变化,并且触发相应的中断。GPIO_EDGE_SEL就是用于配置GPIO输入引脚的触发边沿类型,它的作用是指定在检测到引脚上的信号变化时,触发中断的边沿类型

    GPIO_EDGE_SEL寄存器通常是一个二进制寄存器,每个位对应一个GPIO输入引脚,通过设置或者清除GPIO_EDGE_SEL中相应引脚的位,可以选择上升沿、下降沿或双边沿触发中断

    当GPIO_EDGE_SEL中引脚对应的位被设置为上升沿触发时,只有在引脚从低电平变成高电平时才会触发;如果设置为下降沿触发,则只有引脚信号从高电平变为低电平时才触发中断;如果设置为双边沿触发,则在引脚信号变化的任何边沿都会触发中断。

  • Interrupt mask register,GPIO中断屏蔽寄存器,简称为GPIO_IMR。GPIO接口可以设置为输入模式,用于检测外部事件或者信号的变化,并触发相应的中断。GPIO_IMR用于控制GPIO输入引脚中断的屏蔽,它的作用是允许或屏蔽特定GPIO引脚的中断触发

    GPIO_IMR寄存器通常是一个二进制寄存器,每个位对应一个GPIO引脚。通过设置或清除GPIO_IMR中相应引脚的位,可以选择屏蔽或允许该引脚的中断触发。

    当GPIO_IMR中引脚对应的位被设置为屏蔽状态时,如果该引脚触发了中断事件,处理器将不会响应该中断。相反的,如果该引脚对应的位被清除,那么中断事件将触发处理器执行相应的中断服务程序。

  • Interrupt status register,GPIO中断状态寄存器,简称为GPIO_ISR,用于记录每个GPIO引脚的中断状态信息,表示该引脚是否触发了中断。

    通过读取GPIO_ISR的值,可以获取当前GPIO引脚的中断状态,已确定哪些引脚触发了中断。如果某个引脚触发了中断,则相应的位被设置为1,否则是复位0。该寄存器的作用是提供一个快速的方式来查询和监控GPIO引脚的中断状态。

这8个寄存器在后面开发中我们要经常使用,每个寄存器的宽度是32bit,回想一下之前我们说过GPIO是分组的,每组有多个GPIO,那么如何分组呢?分组名是什么?文章最开始的GPIO5_3是啥意思呢?

在芯片手册28.5记录了上面所说的8种寄存器的地址,以及宽度、读写方式、默认值:

image.png

image.png

不难发现,GPIO一共有5组,每组最多有32个GPIO引脚

GPIO编程

接下来,我们按照前面所说的普适步骤来看看如何找寄存器。首先就是芯片手册中的框图,对于写的不错的芯片手册会有一个框图,表示该GPIO模块还涉及哪些其他模块,如下:

image.png

这个图涉及了不少知识点,我们一一来说。

首先需要CCM模块即时钟控制模块来使能某一组GPIO,然后从GPIO输出的某个引脚,比如上图中的,它可能来自Block,也可能来自GPIO,所以需要设置该引脚的功能

设置引脚功能就是通过IOMUXC模块,每个引脚都有对应的寄存器,又分为2组寄存器,一个是SW_MUX_CTL_PAD,用来设置pin的功能,比如该引脚是用于GPIO还是其他;一个是SW_PAD_CTL_PAD,用来设置pad模块,比如上下拉电阻以及大小等信息。

直接查看芯片手册CCM模块,由于CCM模块比较复杂,它有多个子模块,我们不细读它,在子模块中有GPIOn子模块,如图:

image.png

这里的意思是使用寄存器CCGR1中的第CG13位来控制GPIO1是否使能,后面以此类推。然后在该章的CCM模块找到比如CCGR0寄存器,它的大小如下图:

image.png

再结合前面的CCGR0[CG15]来看,就知道31-30位是设置GPIO2的使能。同时我们发现CG15的部分,默认值是11,那么CGR的值是什么意思呢?从上面可以看出:

image.png 一共有4种值可以设置,其中默认值11就是使能状态。

CCM设置完使能后后,来看IOMUXC模块,对于每个引脚都有对应的寄存器,或者对于一组引脚搞一个寄存器,可以更方便,而寄存器的名字就是GPIO口的名字。

看之前的原理图:

image.png

可以知道该GPIO的名字是GPIO5_3或者SNVS_TEAMPER3,直接在IOMUXC的寄存器找一下,有如下信息:

image.png

可以发现31-5位是保留位,第3-0位是设置复用模式,并且设置为为101就是GPIO模式

最后就是GPIO模块本身了,需要设置几个寄存器。首先就是方向寄存器,用来设置是输入还是输出:

image.png

可以发现对于方向寄存器,设置为0表示输入,设置为1表示输出

然后是数据寄存器,当设置为输出模式时,GPIO模块就会从数据寄存器读取值,然后输出高低电平出去:

image.png

最后就是当GPIO设置为输入时,是从PSR寄存器中读取状态值:

image.png

上面的所有寄存器操作几乎都是位操作,并且寄存器的值都非常明确。

涉及的寄存器地址

在芯片手册上标注的寄存器地址都是物理地址,这里为了方便,我们把设置GPIO5_03的寄存器的值都先归纳一下,方便后续写代码。

由于时钟一直是使能状态,我们就不说了。首先就是IOMUXC模块来设置GPIO5_03这个引脚的功能为GPIO模式,该寄存器为IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3,地址为:229_0000h base + 14h offset = 229_0014h,需要设置的值为二进制的101

然后是设置GPIO5_03引脚的方向,该寄存器是GPIO5_GDIR,查看芯片手册为:

image.png

这里的Base address是啥呢?结合语义我们知道应该是GPIO5的基地址,该基地址可以全局搜索一下得到:

image.png 所以GPIO5_GDIR的地址就是0x20AC004,需要设置第2位的值为1即可。

最后就是设置GPIO5_DR寄存器的值,让其输出高低电平,该寄存器的芯片手册地址为:

image.png

即基地址加0的偏移,所以地址为0x20AC000

LED驱动程序

到这里我们就可以来写操作硬件的驱动程序了,直接看代码:

static int major;
static struct class *led_class;

/* registers */
// 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 ssize_t led_write(struct file *filp, const char __user *buf,
			 size_t count, loff_t *ppos)
{
	char val;
	int ret; 

	/* 从用户空间读取值 */
	ret = copy_from_user(&val, buf, 1);

	if (val) 
	{
		/* 打开led, 写GPIO5_3的值为0*/
		*GPIO5_DR &= ~(1<<3);
	} else {
		/* 关闭led,写GPIO5_3的值为1 */
		*GPIO5_DR |= (1<<3);
	}

	return 1;
}

static int led_open(struct inode *inode, struct file *filp)
{
	/* enable gpio5
	 * configure gpio5_io3 as gpio
	 * configure gpio5_io3 as output 
	 */
	*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 struct file_operations led_fops = {
	.owner 	= THIS_MODULE,
	.write  = led_write,
	.open 	= led_open,
};

/* 入口函数,在加载模块时调用 */
static int __init led_init(void) {
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);

	/* 注册字符设备 */
	major = register_chrdev(0, "100ask_led", &led_fops);

	/* 使用ioremap获取寄存器的虚拟地址 */
	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来反映射内存 */
	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, "100ask_led");
}

module_init(led_init)
module_exit(led_exit);
MODULE_LICENSE("GPL");

对于整体的步骤在上一篇文章中已经介绍过了,核心就是注册一个file_operations结构体到内核,然后自己实现对硬件操作的函数。

这里有一点需要说明一下,就是需要使用ioremap来获取虚拟地址,而不是直接操作物理地址,其他的都没什么可说的,都在上面已经介绍过了,该设置哪些寄存器以及它的值。

总结

在上一篇文章的基础上,本篇文章直接看如何操作硬件。核心点还是不变:

  • 先看原理图,看LED和哪个GPIO引脚相连,以及输出什么电压可以点亮和熄灭LED。
  • 看芯片手册,主要是操作GPIO模块,操作一个GPIO引脚需要使能GPIO组、设置方向、设置工作模式、设置数据等,这些都是通过操作寄存器来实现。
  • 写程序,找到正确的地址,操作虚拟地址,来写寄存器。