• 树莓派编写GPIO驱动程序(详细教程)


    前言

    刚买的树莓派到了,拿到后的第一件事当然是点灯啦!今天就使用树莓派2B通过编写驱动的形式点亮LED小灯,废话少说,直接开干!
    在这里插入图片描述

    硬件准备

    • 树莓派一个
    • 杜邦线若干
    • LED小灯一个
    • 千欧电阻一个

    软件准备

    树莓派需要装载有Linux系统,并且已经安装了Makefile和vim,并且内核中存在build文件夹,如果build文件夹不存在的话可以读一下我之前的文章:
    解决Linux内核目录下没有build文件夹的问题

    前期知识

    在正式编写程序之前我们需要先知道几个比较重要的概念,比如树莓派比较重要的三个地址(地址总线物理地址虚拟地址),怎么通过移位操作寄存器,如何查看芯片手册,如何查看电路原理图等等知识,现在就带上你的好奇心咱们一起开始学习一下一些基本概念!

    在设备驱动程序的开发过程中,我们通常需要跟硬件地址打交道。在不带操作系统的嵌入式平台上(如51单片机,STM32等),我们是可以直接操作的是硬件物理地址。

    但是在基于Linux的嵌入式平台上,我们想要直接控制硬件的地址基本是不可能的。操作系统操作的是虚拟地址,因此我们只有将物理地址映射成为虚拟地址才能对硬件进行操作。


    我使用的硬件平台是树莓派Raspberry Pi 2 Model B,处理器芯片是BCM2836。我想用树莓派直接控制引脚电平高低(简单的GPIO操作)。官方没有提供BCM2837芯片手册,但是提供了BCM2835芯片手册。BCM2837架构是在BCM2835上做了改变,但是基本不变。BCM2835下载地址

    我们从芯片手册中可以看出三个地址,分别是总线地址、ARM虚拟地址、ARM物理地址。我们下面对这三个地址进行一 一介绍。

    总线地址

    总线地址,顾名思义,是与总线相关的,就是总线的地址线或在地址周期上产生的信号, 外设使用的是总线地址。

    物理地址与总线地址之间的关系是由系统的设计决定的。在x86平台上,物理地址与PCI总线地址是相同的。 在其他平台上,也许会有某种转换,通常是线性的转换。

    物理地址

    在存储器里以字节位单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址(Physical Address),又叫实际地址或绝对地址。

    我们在对Linux系统中是无法直接对物理地址进行操作的,我们操作的都是虚拟地址,而虚拟地址和物理地址之间存在映射关系,具体如下图所示:
    在这里插入图片描述

    物理内存只有 512MB,虚拟内存有 4GB,那么肯定存在多个虚拟地址映射到同一个物理地址上去,虚拟地址范围比物理地址范围大的问题处理器自会处理。

    Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚拟地址 。

    在Linux中,虚拟地址和虚拟地址之间的映射是通过ioremapiounmap 函数实现的。

    虚拟地址

    虚拟地址又称为逻辑地址,基于算法实现的软件层面的地址,物理地址通过MMU(分页管理系统)映射后得到的就是虚拟地址,虚拟地址也是我们在编写程序时使用最多的地址。

    地址映射

    我们通过上面的概念可以了解到虚拟地址向物理地址的映射是通过ioremapiounmap 这两个函数实现的。

    ioremap 函数

    ioremap 函 数 用 于 获 取 指 定 物 理 地 址 空 间 对 应 的 虚 拟 地 址 空 间 , 定 义 在arch/arm/include/asm/io.h 文件中,定义如下:

    #define ioremap(cookie,size) __arm_ioremap((cookie), (size), 
    MT_DEVICE)
    
    void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size, 
    unsigned int mtype)
    {
    return arch_ioremap_caller(phys_addr, size, mtype,
    __builtin_return_address(0));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    ioremap 是个宏,有两个参数:cookiesize,真正起作用的是函数__arm_ioremap,此函数有三个参数和一个返回值,这些参数和返回值的含义如下:

    • phys_addr:要映射给的物理起始地址。
    • size:要映射的内存空间大小。
    • mtype:ioremap 的类型,可以选择 MT_DEVICE、MT_DEVICE_NONSHARED、MT_DEVICE_CACHED 和 MT_DEVICE_WC,ioremap 函数选择 MT_DEVICE。
    • 返回值:__iomem 类型的指针,指向映射后的虚拟空间首地址。

    iounmap 函数

    卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射,iounmap 函数原型如下:

    void iounmap (volatile void __iomem *addr)
    
    
    • 1
    • 2

    iounmap 只有一个参数 addr,此参数就是要取消映射的虚拟地址空间首地址。

    LED驱动编写思路

    我们想要控制一个LED实现亮灭的操作其实就是控制树莓派开发板上的一个GPIO输出高低电平。

    而对于树莓派这种具有操作系统的嵌入式来说,我们无法直接对硬件地址进行操作,我们需要通过虚拟地址映射到物理地址上去。而GPIO 的操作就需要用到这种地址映射,进而对相应的寄存器进行操作。

    在ARM架构的SOC中,所有的外围设备都被映射到内存中,所有我们要想控制GPIO的话,我们需要首先知道GPIO的物理地址,进而通过地址偏移的操作获得虚拟地址。

    那我们如何知道硬件对应的物理地址呢?这就需要我们拿到芯片手册和电路原理图了,芯片手册的下载地址我上面已经给出,还没拿到芯片手册的可以向上翻一下。

    注意: 目前市面上流传出来的芯片手册只有BCM2835这一本,这本手册为树莓派B和B+的手册,但是通过寻找资料得知BCM2837其实就是BCM2836的主频升级版;我又去看BCM2836的资料,得知这只不过是BCM2835从32位到64位的升级版;我又去看BCM2835的芯片资料,所以对于树莓派来说这个文档就可以满足你的大部分需求了。

    查看物理地址

    我们查看芯片手册主要关注一下文档90页Register View 中的几个内容
    在这里插入图片描述
    注意: 我们在编写驱动程序的时候,IO空间的起始地址是0x3f000000,加上GPIO的偏移量0x2000000,所以GPIO的物理地址应该是从0x3f200000开始的。

    查询相关寄存器

    我们通过芯片手册知道了物理地址的起始地址,接下来我们只需要找到和GPIO相关的寄存器,然后对其进行操作即可。

    通过阅读手册我们可以知道和GPIO相关的寄存器主要有三个,分别为GPFSELn、GPSETn、GPCLRn

    三个寄存器的介绍主要在文档91~95页,下面是相关寄存器介绍的截图:

    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述
    GPFSEL0(寄存器名)

    GPIO Function Select 0(功能选择:输入或输出);
    
    • 1

    GPSET0 (寄存器名)

    GPIO Pin Output Set 0(将IO口置0);
    
    • 1

    GPSET1(寄存器名)

    GPIO Pin Output Set 1(将IO口置1);
    
    • 1

    GPCLR0(寄存器名)

    GPIO Pin Output Clear 0 (清0)下图的地址是:总线地址(并不是真正的物理地址)
    
    • 1

    配置寄存器

    我们先查看第一个寄存器的值,以此来确定我们需要的GPIO具体是被该寄存器的哪一位控制的。通过查看表Table 6-1 GPIO Register Assignment可以确定每个GPIO对应的寄存器的哪一位。
    在这里插入图片描述
    我们看到表中第一行中内容比较多,但是其他的表中为什么没有呢?其实这是官方偷懒了,其实每一行第三列中都是该内容,即可改为如下:

    000 = GPIO Pin n is an input
    001 = GPIO Pin n is an output 
    100 = GPIO Pin n takes alternate function 0 
    101 = GPIO Pin n takes alternate function 1 
    110 = GPIO Pin n takes alternate function 2 
    111 = GPIO Pin n takes alternate function 3 
    011 = GPIO Pin n takes alternate function 4 
    010 = GPIO Pin n takes alternate function 5 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    所以我们的引脚就可以通过该表得到具体在哪一位,比如说我们想要确定GPIO4在寄存器GPFSEL0 中的哪一位,我们就可以查看表中的FSEL4 对应的行,具体如下:
    在这里插入图片描述
    加上我们上面所得通用Description ,可以确定如果我们想要对GPIO4进行配置我们需要对哪些位写入什么内容。

    Description内容:

    000 = GPIO Pin 4 is an input
    001 = GPIO Pin 4 is an output 
    100 = GPIO Pin 4 takes alternate function 0 
    101 = GPIO Pin 4 takes alternate function 1 
    110 = GPIO Pin 4 takes alternate function 2 
    111 = GPIO Pin 4 takes alternate function 3 
    011 = GPIO Pin 4 takes alternate function 4 
    010 = GPIO Pin 4 takes alternate function 5 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    比如我现在需要将GPIO4配置成输出引脚,通过上面的Description 和表格我们可以知道我们需要将141312位依次写入001 这样我们就明白了我们的意图,那么用代码如何实现呢?

    大家可以先思考一下自己的想法,我这里给出一个思路,供大家参考:

    //配置pin4引脚为输出引脚        
    *GPFSEL0 &=~(0x6 <<12); // 把bit13 、bit14置为0  
    //0x6是110  <<12左移12位 ~取反 &按位与
    *GPFSEL0 |=~(0x1 <<12); //把12置为1   |按位或
    
    • 1
    • 2
    • 3
    • 4

    代码讲解:

    我们知道我们对一个特定的位想要写入1,那么我们就需要使用该位为1,其他位为0的数值上原来的数值,具体如下:

    比如原来存在一个四位的2进制数:0101
    我们现在想要该数的从高位数第三位变成1
    
    我们是不是可以使用 0101 | 0010 
    最后我们得到:0111
    达到目的!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    那如果我们想要将某位变成0,其他位不变该如何操作呢?

    我们可以使用只有该位为0,其他位都为1的数上原来的数,具体如下:

    比如原来存在一个四位的2进制数:0110
    我们现在想要该数的从高位数第三位变成0
    
    我们是不是可以使用 0100 & 1101 
    最后我们得到:0100
    达到目的!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    知道了这个思想我们再来看上面的两个语句就不难了,具体如下:

    0x7E20 &=~(0x6 <<12);//这里为了方便,我们使用的是16位的数
    					 //实际上树莓派中是32位的,原理相同
    
    0x6即为2进制的1100x6左移12位可以得到:
    0110 0000 0000 0000
    我们对上面的数值进行取反,得到:
    1001 1111 1111 1111
    
    通过我上面举的简单的例子我们知道,任何数与该数值进行与操作都会将1413位变成0
    其他位保持不变,所以0x7E20 &=~(0x6 <<12)操作如下:
    0111 1110 0010 0000 & 1001 1111 1111 1111
    
    得到:0001 1110 0010 0000
    
    达到目的!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这样我们就成功的将1413位变成了0,下一步就是将12位变成1即可,将12位变成1是通过下面的语句实现的:

    *GPFSEL0 |=~(0x1 <<12); //把12置为1   |按位或
    
    • 1

    这句应该比较好理解,配合我上面举得例子,大家可以自己思考一下为什么这么写!

    至此,我们成功配置好了寄存器GPFSEL0,下一步我们还有两个寄存器需要配置,另外两个寄存器的配置和这个基本相同,我就不再做详细讲解了,大家可以自己尝试配置一下,我后面会直接给出代码!

    物理地址的映射

    至此我们就得到了GPIO对应的虚拟地址,下一步就是将虚拟地址映射到物理上。(不知道如何得到的可以看章节查看物理地址

    这里我们就需要用到上面基础知识里面讲到的Linux系统的MMU内存虚拟化管理,映射到虚拟地址上。用到了一个函数ioremap

    //物理地址转换成虚拟地址,io口寄存器映射成普通内存单元进行访问
     GPFSEL0=(volatile unsigned int *)ioremap(0x3f200000,4);
     GPSET0 =(volatile unsigned int *)ioremap(0x3f20001C,4);
     GPCLR0 =(volatile unsigned int *)ioremap(0x3f200028,4);   //4是4个字节
    
    • 1
    • 2
    • 3
    • 4

    具体ioremap 第一个参数需要填什么是需要大家查看下面的这个表的!
    在这里插入图片描述
    想必看到这里大家应该就比较清楚我们是如何对LED相关寄存器操作,下一步就是将我们对寄存器的代码加入到框架中,学到现在Linux驱动的框架我就不需要再啰嗦了吧,直接上代码!

    /* 打开设备*/
    static int chrdevbase_open(struct inode *inode, struct file *filp)
    {
     
    	return 0;
    }
     
    /*关闭/释放设备*/
    static int chrdevbase_release(struct inode *inode, struct file *filp)
    {
     
    	return 0;
    }
     
    /* 从设备读取数据 */
    static ssize_t chrdevbase_read(struct file *filp, __user char *buf, size_t count, loff_t *ppos)
    {
     
    	return 0;
    }
     
    /*向设备写数据 */
    static ssize_t chrdevbase_write(struct file *filp, const char __user *buf,
    								size_t count, loff_t *ppos)
    {
    	int ret = 0;
    	// printk("chrdevbase_write\r\n");
    	ret = copy_from_user(writebuf, buf, count);
    	if (ret == 0)
    	{
    		printk("kernel recevdata:%s\r\n", writebuf);
    	}
    	else
    	{
    	}
    	return 0;
    }
     
    /*字符设备,操作集合*/
    static struct file_operations chrdevbase_fops = {
    	.owner = THIS_MODULE,
    	.open = chrdevbase_open,
    	.release = chrdevbase_release,
    	.read = chrdevbase_read,
    	.write = chrdevbase_write,
     
    };
     
    /* 驱动入口函数 */
    static int __init chrdevbase_init(void)
    {
    
    }
     
    /*驱动出口函数*/
    static void __exit chrdevbase_exit(void)
    {
     
    	printk("chrdevbase_exit\r\n");
    	/*注销字符设备*/
    	unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
    }
     
     
    /*  将上面两个函数指定为驱动的入口和出口函数 */
    module_init(chrdevbase_init); 
    module_exit(chrdevbase_exit);
     
    /* LICENSE和作者信息*/
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("jamesbin");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    编写LED驱动代码

    首先我们需要在入口函数中创建设备号、创建类、映射地址等操作,所以我们的init可以这样写:

    static int __init led_init(void)
    {
    	int ret;
    	devno = MKDEV(major,minor);  //创建设备号
    	
    	ret   = register_chrdev(major, module_name,&pin4_fops);  //注册驱动  告诉内核,把这个驱动加入到内核驱动的链表中
    	
    	pin4_class=class_create(THIS_MODULE,"myfirstdemo");//让代码在dev下自动>生成设备
    
    	pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name);  //创建设备文件
    	
    
    	GPFSEL0=(volatile unsigned int *)ioremap(0x3f200000,4);
    	GPSET0 =(volatile unsigned int *)ioremap(0x3f20001C,4);
    	GPCLR0 =(volatile unsigned int *)ioremap(0x3f200028,4);
    
    	printk("insmod driver pin4 success\n");
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在出口函数中对我们申请到的资源和映射的内存进行释放,具体代码如下:

    void __exit led_exit(void)
    {
        iounmap(GPFSEL0);
        iounmap(GPSET0);
        iounmap(GPCLR0); //卸载驱动时释放地址映射
        
        device_destroy(pin4_class,devno);
        class_destroy(pin4_class);
        unregister_chrdev(major, module_name);  //卸载驱动
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    写完入口和出口函数下一步我们就需要开始编写操作函数了,即openwrite等,具体代码如下,不懂的话可以看代码中的注释!

    led_drv.c

    #include             //file_operations声明
    #include     //module_init  module_exit声明
    #include       //__init  __exit 宏定义声明
    #include         //class  devise声明
    #include    //copy_from_user 的头文件
    #include      //设备号  dev_t 类型声明
    #include           //ioremap iounmap的头文件
    
    
    static struct class *led_class;
    static struct device *led_class_dev;
    
    static dev_t devno;                //设备号
    static int major =231;             //主设备号
    static int minor =0;               //次设备号
    static char *module_name="led_drv";   //模块名
    
    volatile unsigned int* GPFSEL0 = NULL;
    volatile unsigned int* GPSET0   = NULL;
    volatile unsigned int* GPCLR0   = NULL;
    //这三行是设置寄存器的地址
    //volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值
    
    //led_open函数
    static int led_open(struct inode *inode,struct file *file)
    {
            printk("led_open\n");  //内核的打印函数和printf类似
           
            //配置pin4引脚为输出引脚        
            *GPFSEL0 &=~(0x6 <<12); // 把bit13 、bit14置为0  
            //0x6是110  <<12左移12位 ~取反 &按位与
            *GPFSEL0 |=(0x1 <<12); //把12置为1   |按位或
            
            return 0;
    
    }
    //read函数
    static int led_read(struct file *file,char __user *buf,size_t count,loff_t *ppos)
    {
            printk("led_read\n");  //内核的打印函数和printf类似
    
            return 0;
    }
    
    //led_write函数
    static ssize_t led_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
    {
            int usercmd;
            printk("led_write\n");  //内核的打印函数和printf类似
            
            //获取上层write函数的值                
            copy_from_user(&usercmd,buf,count); //将应用层用户输入的指令读如usercmd里面
            //根据值来操作io口,高电平或者低电平
            if(usercmd == 1){
                    printk("set 1\n");
                    *GPSET0 |= (0x1 << 4);
            }
            else if(usercmd == 0){
                    printk("set 0\n");
                    *GPCLR0 |= (0x1 << 4);
            }
            else{
                    printk("undo\n");
            }
            return 0;
    }
    
    static struct file_operations led_fops = {
    
            .owner = THIS_MODULE,
            .open  = led_open,
            .write = led_write,
            .read  = led_read,
    };
    
    //static限定这个结构体的作用,仅仅只在这个文件。
    int __init led_init(void)   //真实的驱动入口
    {
            int ret;
            devno = MKDEV(major,minor);  //创建设备号
            ret   = register_chrdev(major, module_name,&led_fops);  //注册驱动  告诉内核,把这个驱动加入到内核驱动的链表中
            led_class=class_create(THIS_MODULE,"myfirstdemo");//让代码在dev下自动>生成设备
            led_class_dev =device_create(led_class,NULL,devno,NULL,module_name);  //创建设备文件
            
            GPFSEL0=(volatile unsigned int *)ioremap(0x3f200000,4);
            GPSET0 =(volatile unsigned int *)ioremap(0x3f20001C,4);
            GPCLR0 =(volatile unsigned int *)ioremap(0x3f200028,4);
    
            printk("insmod driver led success\n");
            return 0;
    }
    
    void __exit led_exit(void)
    {
    
            iounmap(GPFSEL0);
            iounmap(GPSET0);
            iounmap(GPCLR0); //卸载驱动时释放地址映射
    
            device_destroy(led_class,devno);
            class_destroy(led_class);
            unregister_chrdev(major, module_name);  //卸载驱动
    }
    module_init(led_init);  //入口,内核加载驱动的时候,这个宏会被调用,去调用pin4_drv_init这个函数
    module_exit(led_exit);
    MODULE_LICENSE("GPL");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106

    至此我们的LED驱动程序就已经编写完成了!

    编写Makefile

    Makefile

    KVERSION = $(shell uname -r)
    KERN_DIR = /lib/modules/$(KVERSION)/build 
    
    all:
    	make -C $(KERN_DIR) M=`pwd` modules 
    	
    led_test:led_test.c
    	gcc led_test.c -o led_test
    clean:
    	make -C $(KERN_DIR) M=`pwd` modules clean
    	rm -rf modules.order
    	rm led_test
    
    obj-m	+= led_drv.o
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    编写测试程序

    led_test.c

    #include 
    #include      //open
    
    
    /*
     * led_test on
     * led_test off
     */
    
    int main(int argc, char **argv)
    {
    	int fd;
    	int val = 0;
    	if(argc != 2)
    	{
    		printf("Usage: \n");
    		printf("%s: \n", argv[0]);     //<> must have on | off
    		return 0;
    	}
    	
    	fd = open("/dev/led_drv",O_RDWR);
    	if( fd < 0 )
    	{
    		printf("can not open\n");
    	}
    	
    	if( strcmp(argv[1],"on") == 0 )
    	{
    		val = 1;
    		printf("led on\n");
    	}
    	else if( strcmp(argv[1],"off") == 0 )
    	{
    		val = 0;
    		printf("led off\n");
    	}
    	//write(fd, &val, 4);
    	write(fd, &val,4); //val类型是int  所以 写4
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    执行代码

    make编译drv文件:

    make
    
    • 1

    编译led_test文件

    make led_test
    
    • 1

    装载驱动

    sudo insmod led_drv.ko
    
    • 1

    测试程序

    sudo ./led_test on     //开灯 
    sudo ./led_test off    //关灯
    
    • 1
    • 2

    至此你的小灯应该已经被你点亮了!成功的话记得一键三连哦!!!

    👇点击下方公众号卡片获取资料👇
  • 相关阅读:
    编程狂人|妙用Java 8中的 Function接口 消灭if...else...
    C的char溢出
    Kotlin--内置函数的总结
    基于STM32CUBEMX驱动TMOS模块STHS34PF80(5)----配置嵌入式函数
    Java面向对象编程(六)
    【老生谈算法】matlab实现图像增强算法源码——图像增强算法
    算法题:买卖股票的最佳时机 II (贪心算法解决股票问题)
    工业 web4.0 的 UI 卓越非凡
    Istio实践(2)- 流量控制及服务间调用
    CentOS有IP地址,连接不上Xshell或使用Xshell时突然断开
  • 原文地址:https://blog.csdn.net/qq_45172832/article/details/125864843