【北京迅为】《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灭。

相关推荐
Aileen_0v018 分钟前
【AI驱动的数据结构:包装类的艺术与科学】
linux·数据结构·人工智能·笔记·网络协议·tcp/ip·whisper
qq_4597300337 分钟前
4-3 MCU中ARM存储器的作用
arm开发·单片机·嵌入式硬件
州周1 小时前
Ftp目录整个下载
linux·服务器·数据库
Jackey_Song_Odd1 小时前
Ubuntu 24.04.1 解决部分中文字符(门、径)显示错误的问题
linux·ubuntu
kaixin_learn_qt_ing1 小时前
Linux export命令
linux
余额不足121381 小时前
C语言基础十六:枚举、c语言中文件的读写操作
linux·c语言·算法
冷曦_sole1 小时前
linux-19 根文件系统(一)
linux·运维·服务器
AI大模型学徒1 小时前
Linux(二)_清理空间
linux·运维·服务器
云川之下1 小时前
【linux】 unshare -user -r /bin/bash命令详解
linux·bash·unshare
热心市民运维小孙2 小时前
Ubuntu重命名默认账户
linux·ubuntu·excel