Bootstrap

Linux嵌入式点亮一个led

当我们要操控一个硬件时,需要用应用程序去操控,我们可以编写对应的驱动程序去对硬件进行操作。所以这次的实例中包含led应用程序和驱动程序的编写。

相关的硬件

先看开发板的原理图可知,当GPIO5_3为低电平时led点亮,否则不亮

由上图可知,点亮led需要使用GPIO5_3引脚,使用应该通过CCM给GPIO5使能,查阅芯片手册可知,GPIO5默认使能,无需设置

一个引脚可能被复用,所以设置GPIO5_IO03为GPIO模式,阅读芯片手册可知,将该寄存器的MUX_MODE部分设置为101则为GPIO模式

GPIO有输出也有输入模式,由开发板的原理图可知应该将GPIO设置为输出模式,由芯片手册可知,将GPIO5_GDIR寄存器的对应位设置为1则为输出模式

当准备好以上工作时就要给GPIO设置输出电平,由原理图可知,对寄存器GPIO5_DR对应位设置为1或0就可以设置其输出电平

重要函数介绍

ioremap函数

建立物理地址和虚拟地址之间的映射关系,让内核可以通过虚拟地址来访问硬件设备的物理地址空间,从而实现对硬件设备的控制和数据交互

void __iomem *ioremap(resource_size_t phys_addr, unsigned long size);

phys_addr:这是一个 resource_size_t 类型的参数,表示要映射的硬件设备的物理起始地址。这个地址通常可以从硬件设备的数据手册中获取。

size:这是一个 unsigned long 类型的参数,表示要映射的物理地址空间的大小,单位是字节。

iounmap函数

在使用完 ioremap 映射的虚拟地址后,必须调用 iounmap 函数解除映射,以避免内存泄漏。

void iounmap(void __iomem *addr);

 addr:这是一个 void __iomem * 类型的指针,它指向之前通过 ioremap 函数映射得到的虚拟地址。__iomem 是一个内核定义的标记,用于表明该指针指向的是 I/O 内存区域。

 

代码示例 

驱动程序

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/delay.h>
#include <linux/poll.h>
#include <linux/mutex.h>
#include <linux/wait.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <asm/io.h>

static int major;		//设备号
static struct class *led_class;


static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
static volatile unsigned int *GPIO5_GDIR;
static volatile unsigned int *GPIO5_DR;

static ssize_t led_drv_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){		//不为零点亮
		*GPIO5_DR &= ~(1<<3);
	}else{
		*GPIO5_DR |= (1<<3);
	}
	return 1;
}

static int led_drv_open(struct inode *node, 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);   //将该位置为1为输出模式
	return 0;
}

static struct file_operations led_fods = {
	.owner		= THIS_MODULE,
	.open 		= led_drv_open,
	.write 		= led_drv_write,
};


//出入口函数
static int __init led_init(void)
{
	major=register_chrdev(0,"myled",&led_fods);
	
	//将物理地址转化为虚拟地址使用
	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)
{
	//解除之前通过 ioremap 建立的物理地址到虚拟地址的映射关系
	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);
MODULE_LICENSE("GPL");


当我们定义指针是加入了一个类型修饰符, volatile是一个类型修饰符,用于告诉编译器该变量的值可能会以编译器无法预知的方式被改变,因此编译器在使用这个变量时每次都要从内存中读取其值,而不是使用之前缓存的副本。

i = 1;
i = 2;

当有以上例子时,如果不加volatile,编译器会直接读取i=2,直接跳过了i=1,但是软件和硬件出现这种情况区别很大。

应用程序

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>


// ledtest /dev/myled on
// ledtest /dev/myled off

int main(int argc, char **argv)
{
	int fd;
	char status = 0;
	
	if (argc != 3)
	{
		printf("Usage: %s <dev> <on|off>\n", argv[0]);
		printf("  eg: %s /dev/myled on\n", argv[0]);
		printf("  eg: %s /dev/myled off\n", argv[0]);
		return -1;
	}
	// open
	fd = open(argv[1], O_RDWR);
	if (fd < 0)
	{
		printf("can not open %s\n", argv[0]);
		return -1;
	}

	// write
	if (strcmp(argv[2], "on") == 0)
	{
		status = 1;
	}

	write(fd, &status, 1);
	return 0;	
}

Makfile的编写

在之前的hello驱动程序进行更改

KERN_DIR=/home/book/100ask_imx6ull-sdk/Linux-4.9.88

all:
    make -C $(KERN_DIR) M=$(shell pwd) modules
    $(CROCROSS_COMPILESS)gcc -o led_test led_test.c
clean:
    make -C $(KERN_DIR) M=$(shell pwd) modules clean
    rm -rf modules.order
    rm -r led_test

obj_m +=led_drv.o

ps:以上代码参考韦东山老师的教程

;