【北京迅为】《i.MX8MM嵌入式Linux开发指南》-第三篇 嵌入式Linux驱动开发篇-第四十二章 LED驱动实验

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函数。

完整的驱动文件如下所示:

cpp 复制代码
/*
 * @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 编写应用程序

编写应用程序如下所示:

cpp 复制代码
#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灭。

相关推荐
ye1501277745534 分钟前
DC6v-36V转3.2V1A恒流驱动芯片WT7017
单片机·嵌入式硬件·其他
炫友呀7 小时前
Centos 更新/修改宝塔版本
linux·运维·centos
花小璇学linux11 小时前
imx6ull-驱动开发篇24——Linux 中断API函数
linux·驱动开发·嵌入式软件
林开落L11 小时前
库制作与原理(下)
linux·开发语言·centos·库制作与原理
wxy31912 小时前
嵌入式LINUX——————TCP并发服务器
java·linux·网络
Castamere12 小时前
配置 Linux 终端 (zsh)
linux
bai54593613 小时前
STM32 软件I2C读写MPU6050
stm32·单片机·嵌入式硬件
小韩博13 小时前
metasploit 框架安装更新遇到无法下载问题如何解决
linux·网络安全·公钥·下载失败
长臂人猿13 小时前
JVM常用工具:jstat、jmap、jstack
linux·运维·jvm
轻松Ai享生活14 小时前
揭秘 linux:一张图看懂系统配置的核心
linux