i.MX8MM处理器采用了先进的14LPCFinFET工艺,提供更快的速度和更高的电源效率;四核Cortex-A53,单核Cortex-M4,多达五个内核 ,主频高达1.8GHz,2G DDR4内存、8G EMMC存储。千兆工业级以太网、MIPI-DSI、USB HOST、WIFI/BT、4G模块、CAN、RS485等接口一应俱全。H264、VP8视频硬编码,H.264、H.265、VP8、VP9视频硬解码,并提供相关历程,支持8路PDM接口、5路SAI接口、2路Speaker。系统支持Android9.0(支持获取root限)Linux4.14.78+Qt5.10.1、Yocto、Ubuntu20、Debian9系统。适用于智能充电桩,物联网,工业控制,医疗,智能交通等,可用于任何通用工业和物联网应用、
【公众号】迅为电子
【粉丝群】258811263(加群获取驱动文档+例程)
第四十二章 LED驱动实验
本章导读
本章节将以实践课的方式讲解LED驱动,将从硬件原理图的查看,编写驱动程序,运行测试三个步骤进行讲解。
本章内容对应视频讲解链接(在线观看):
第一个相对完整的驱动实践编写 → https://www.bilibili.com/video/BV1Vy4y1B7ta?p=13
程序源码在网盘资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\2.驱动程序例程\04-LED驱动实验”路径下。
42.1 实验需求分析
实验需求:
1.使用杂项设备完成一个LED驱动。
2.完成一个上层测试应用。
应用要求:
在上层应用中传入参数1为打开LED,传入参数0为关闭LED。
需求分析:
- 首先完成一个LED的驱动使用的是杂项设备,那么按照杂项设备的注册流程来填充结构体,参考第三十九章节 Linux MISC的内容。
- 杂项设备注册完毕,我们还要和硬件关联起来,要用到设备节点,因为设备节点是应用和驱动的桥梁,设备节点是通过杂项设备注册生成的。
- 我们要操作LED,就要完成相关的函数了。比如说,我们要完成read函数,open函数,做驱动就是做的这几个函数。参考第四十章节 Linux用户层和内核层的内容。
- 我们要完成上层应用要传入参数,就涉及到了应用层向内核层传递数据,我们传递数据不能直接传,我们要用到两个函数,用到copy_to_user()和copy_from_user()。
那么实验需求给大家分析完了。
42.2 硬件分析
42.2.1 硬件原理图分析
iTOP-i.MX8MM开发板是底板加核心板的结构,底板原理图在“8MM开发板\iTOP-i.MX8MM开发板\01-i.MX8MM开发板光盘资料\20210830\01-硬件资料\02-原理图\底板原理图”下载。iTOP-i.MX8MM开发板底板上默认的LED灯被使用了,所以我们可以找个空闲的gpio引脚连接led灯来进行实验。打开底板原理图找到U60 插槽,如下图所示:
我们将led小灯正极插在GPIO_IO13上,负极插在2号引脚接地,硬件连接如下图所示:
42.2.2 芯片手册分析
打开iTOP-i.MX8MM光盘资料“8MM开发板\iTOP-i.MX8MM开发板\01-i.MX8MM开发板光盘资料\20210830\01-硬件资料\03-参考手册”,因为我们的内核里面已经的驱动了,像复用关系的寄存器,电气属性的寄存器,就可以不用设置了,直接设置数据寄存器和方向寄存器就可以了。我们在此文档中查找引脚GPIO_IO13的数据寄存器和方向寄存器,如下图所示,数据寄存器的基地址是0x30200000,方向寄存器的地址是0x30200004,这些地址在我们后面编写驱动的时候需要使用。
在驱动开发调试过程,我们可以通过 io 命令访问寄存器,如需判断 iomux, gpio direction/电平,提高/减小驱动强度,使能施密特触发,这是一个高效又准确的调试手段。
42.3 编写LED驱动
我们以iTOP-iMX8MM开发板为例编写驱动文件,实现控制开发板上面LED的效果。
我们在ubuntu的home/topeet/imx8mm/04目录下新建led.c文件,可以在上次实验的驱动代码基础上进行修改,以下代码为完整的驱动代码。
我们已经学会了杂项设备驱动编写的基本流程,其实需求已经完成了一半了,我们已经注册了杂项设备,并生成了设备节点。接下来我们要完成控制LED的逻辑操作,那么控制LED就涉及到了对寄存器的操作,但是对寄存器的操作我们是不能直接访问的,因为linux不能直接访问我们的物理地址,需要把物理地址先映射成虚拟地址,我们完成这一步转换需要用到ioremap函数。
完整的驱动文件如下所示:
/*
* @Descripttion: 基于杂项设备的LED驱动
*/
#include <linux/init.h> //初始化头文件
#include <linux/module.h> //最基本的文件,支持动态添加和卸载模块。
#include <linux/miscdevice.h> //包含了miscdevice结构的定义及相关的操作函数。
#include <linux/fs.h> //文件系统头文件,定义文件表结构(file,buffer_head,m_inode等)
#include <linux/uaccess.h> //包含了copy_to_user、copy_from_user等内核访问用户进程内存地址的函数定义。
#include <linux/io.h> //包含了ioremap、iowrite等内核访问IO内存等函数的定义。
#include <linux/kernel.h> //驱动要写入内核,与内核相关的头文件
#define GPIO1_IO13 0x30200000 //led数据寄存器物理地址
#define GPIO1_IO13_GDIR 0x30200004 //led数据寄存器方向寄存器物理地址
unsigned int *vir_gpio1_io13; //存放映射完的虚拟地址的首地址
unsigned int *vir_gpio1_io13_gdir; //存放映射完的虚拟地址的首地址
/**
* @name: misc_read
* @test: 从设备中读取数据,当用户层调用函数read时,对应的,内核驱动就会调用这个函数。
* @msg:
* @param {structfile} *file file结构体
* @param {char__user} *ubuf 这是对应用户层的read函数的第二个参数void *buf
* @param {size_t} size 对应应用层的read函数的第三个参数
* @param {loff_t} *loff_t 这是用于存放文件的偏移量的,回想一下系统编程时,读写文件的操作都会使偏移量往后移。
* @return {*} 当返回正数时,内核会把值传给应用程序的返回值。一般的,调用成功会返回成功读取的字节数。
如果返回负数,内核就会认为这是错误,应用程序返回-1
*/
ssize_t misc_read(struct file *file, char __user *ubuf, size_t size, loff_t *loff_t)
{
printk("misc_read\n ");
return 0;
}
/**
* @name: misc_write
* @test: 往设备写入数据,当用户层调用函数write时,对应的,内核驱动就会调用这个函数。
* @msg:
* @param {structfile} * filefile结构体
* @param {constchar__user} *ubuf 这是对应用户层的write函数的第二个参数const void *buf
* @param {size_t} size 对应用户层的write函数的第三个参数count。
* @param {loff_t} *loff_t 这是用于存放文件的偏移量的,回想一下系统编程时,读写文件的操作都会使偏移量往后移。
* @return {*} 当返回正数时,内核会把值传给应用程序的返回值。一般的,调用成功会返回成功读取的字节数。
如果返回负数,内核就会认为这是错误,应用程序返回-1。
*/
ssize_t misc_write(struct file *file, const char __user *ubuf, size_t size, loff_t *loff_t)
{
/*应用程序传入数据到内核空间,然后控制LED的逻辑,在此添加*/
// kbuf保存的是从应用层读取到的数据
char kbuf[64] = {0};
// copy_from_user 从应用层传递数据给内核层
if (copy_from_user(kbuf, ubuf, size) != 0)
{
// copy_from_user 传递失败打印
printk("copy_from_user error \n ");
return -1;
}
//打印传递进内核的数据
printk("kbuf is %d\n ", kbuf[0]);
*vir_gpio1_io13_gdir |= (1 << 13);
if (kbuf[0] == 1) //传入数据为1 ,LED亮
{
*vir_gpio1_io13 |= (1 << 13);
}
else if (kbuf[0] == 0)
{ //传入数据为0,LED灭
*vir_gpio1_io13 &= ~(1 << 13);
}
return 0;
}
/**
* @name: misc_release
* @test: 当设备文件被关闭时内核会调用这个操作,当然这也可以不实现,函数默认为NULL。关闭设备永远成功。
* @msg:
* @param {structinode} *inode 设备节点
* @param {structfile} *file filefile结构体
* @return {0}
*/
int misc_release(struct inode *inode, struct file *file)
{
printk("hello misc_relaease bye bye \n ");
return 0;
}
/**
* @name: misc_open
* @test: 在操作设备前必须先调用open函数打开文件,可以干一些需要的初始化操作。
* @msg:
* @param {structinode} *inode 设备节点
* @param {structfile} *file filefile结构体
* @return {0}
*/
int misc_open(struct inode *inode, struct file *file)
{
printk("hello misc_open\n ");
return 0;
}
//文件操作集
struct file_operations misc_fops = {
.owner = THIS_MODULE,
.open = misc_open,
.release = misc_release,
.read = misc_read,
.write = misc_write,
};
//miscdevice结构体
struct miscdevice misc_dev = {
.minor = MISC_DYNAMIC_MINOR,
.name = "hello_misc",
.fops = &misc_fops,
};
static int misc_init(void)
{
int ret;
//注册杂项设备
ret = misc_register(&misc_dev);
if (ret < 0)
{
printk("misc registe is error \n");
}
printk("misc registe is succeed \n");
//将物理地址转化为虚拟地址
vir_gpio1_io13 = ioremap(GPIO1_IO13, 4);
if (vir_gpio1_io13 == NULL)
{
printk("vir_gpio1_io13 ioremap is error \n");
return EBUSY;
}
printk("vir_gpio1_io13 ioremap is ok \n");
vir_gpio1_io13_gdir = ioremap(GPIO1_IO13_GDIR, 4);
if (vir_gpio1_io13 == NULL)
{
printk("GPIO1_IO13_GDIR ioremap is error \n");
return EBUSY;
}
printk("GPIO1_IO13_GDIR ioremap is ok \n");
return 0;
}
static void misc_exit(void)
{
//卸载杂项设备
misc_deregister(&misc_dev);
iounmap(vir_gpio1_io13);
iounmap(vir_gpio1_io13_gdir);
printk(" misc gooodbye! \n");
}
module_init(misc_init);
module_exit(misc_exit);
MODULE_LICENSE("GPL");
42.4 编写应用程序
编写应用程序如下所示:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
int fd;
char buf[64] = {0};//定义buf缓存
//打开设备节点
fd = open("/dev/hello_misc",O_RDWR);
if(fd < 0)
{
//打开设备节点失败
perror("open error \n");
return fd;
}
// atoi()将字符串转为整型,这里将第一个参数转化为整型后,存放在buf[0]中
buf[0] = atoi(argv[1]);
//把缓冲区数据写入文件中
write(fd,buf,sizeof(buf));
printf("buf is %d\n",buf[0]);
close(fd);
return 0;
}
将app.c文件拷贝到Ubuntu的/home/topeet/imx8mm/04目录下,编译app.c,如下图所示:
42.5 编译驱动及运行测试
我们将42.4章节编写的驱动文件led.c编译成模块,将上次编译file_operation的Makefile文件和build.sh文件拷贝到led.c同级目录下,修改Makefile为:
obj-m += led.o
KDIR:=/home/topeet/linux/linux-imx
PWD?=$(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules ARCH=arm64
clean:
make -C $(KDIR) M=$(PWD) clean
文件如下图所示:
输入命令编译驱动如下图所示:
驱动编译完,我们进入到共享目录,加载驱动模块如图所示:
insmod beep.ko
然后我们输入命令“./app 1”打开LED,LED亮;输入命令“./app 0”关闭LED,LED灭。