Linux驱动入门 —— 利用引脚号操作GPIO进行LED点灯

目录

一、字符设备驱动程序框架

编写驱动程序的步骤:

对于 LED 驱动,我们想要什么样的接口?

LED 驱动能支持多个板子的基础:分层思想

二、Linux驱动如何指向一个GPIO

直接通过寄存器来操作GPIO

利用引脚号操作GPIO

IMX6ULL引脚获取

三、Linux的统一接口 — GPIO子系统

为什么需要统一接口

四、GPIO子系统函数介绍

(1)gpio_request()

(2)gpio_free()

(3)gpio_direction_input()

(4)gpio_direction_output()

(5)gpio_get_value()

(6)gpio_set_value()

五、LED驱动代码

驱动层代码:

应用层代码:

Makefile

上机测试:


一、字符设备驱动程序框架

图 6.1 中驱动层访问硬件外设寄存器依靠的是 ioremap 函数去映射到寄存 器地址,然后开始控制寄存器。

编写驱动程序的步骤:

  1. 确定主设备号,也可以让内核分配
  2. 定义自己的 file_operations 结构体
  3. 实现对应的 drv_open/drv read/drv write 等函数,填入 file operations 结构体
  4. 把 file_operations 结构体告诉内核: register_chrdev
  5. 谁来注册驱动程序啊? 得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
  6. 有入口函数就应该有出口函数: 卸载驱动程序时,出口函数调用unregister_chrdev
  7. 其他完善:提供设备信息,自动创建设备节点: class_create,device_create

对于 LED 驱动,我们想要什么样的接口?

LED 驱动能支持多个板子的基础:分层思想

把驱动拆分为通用的框架(leddrv.c)、具体的硬件操作(board_X.c):

以面向对象的思想,改进代码,抽象出一个结构体:

每个单板相关的 board_X.c 实现自己的 led_operations 结构体,供上层 的 leddrv.c 调用:

二、Linux驱动如何指向一个GPIO

在编写驱动程序的时候,如果我们需要进行点灯操作,首先要知道控制的是哪一个引脚才可以真正的操作LED。我们通过在原理图中查找发现,LED是由GPIO5_3控制。

直接通过寄存器来操作GPIO

(1)我们在学习入门视频的时候,常常会看到他们使用ioremap()函数对寄存器进行映射,然后直接操作寄存器。不再要使用这个寄存器的时候,就调用iounmap()函数进行释放。
(2) 这样编写毫无疑问,非常原始,就像是在写51单片机的程序。但是不同的在于,51单片机的寄存器并不多,所以直接操作寄存器并不麻烦。而i.max6ull这个级别的芯片,寄存器一大堆,再直接进行寄存器操作,无疑非常麻烦。
(3)我上一篇写的博客就是直接利用寄存器来操作GPIO,链接如下:Linux驱动入门 —— LED点灯驱动程序-CSDN博客

想要深入学习的,可以看看正点原子,或者韦东山老师的驱动教学视频。

利用引脚号操作GPIO

(1)从上面的原理图,我们知道了LED是由GPIO5_3控制之后,就可以直接开始操作了吗?
(2)不对,在 Linux 中,GPIO 的标识和控制通常是通过引脚号来进行的,引脚号是用于唯一标识特定的 GPIO 引脚。
(3)如果我们有stm32,msp430这种裸机开发经验,就会发现,不同的芯片对于GPIO的名字定义是不同的。比如STM32将引脚定义成PA0,PB4这种。但是如果是MSP430单片机,他对引脚的定义是P2.3,P1.0这样的。不同厂家对自己的芯片GPIO名字不同。
(4)这种GPIO名字不同,会导致什么结果呢?这样会让驱动开发人员总是要记,不同的芯片的命名规则,显然是非常麻烦的。于是,Linux规定了,我不管你是什么芯片厂家,不管你怎么命名,你如果要跑Linux,就必须将引脚变成一个数字,这个数字叫做引脚号。驱动开发人员,只需要知道这个引脚对应的引脚号,就可以进行操作了。而把这个引脚变成引脚号的过程,就是由芯片原厂的工程师来做了。
(5)关于引脚号的获取,最简单的办法就是,直接联系厂家询问。比如如下是飞腾的芯片,他们的引脚映射表。

IMX6ULL引脚获取

现在我的这个IMX6ULL开发板要控制GPIO5_3,而且我找不到他们的引脚映射表,那么他这个引脚号是多少呢?
如果找不到映射表,我们连接上开发板

输入指令:cat /sys/kernel/debug/gpio

即可获得GPIO的映射表,以及他的起始地址。

(1)这个时候有人可能就会认为,这里搜索到的gpiochip5就是GPIO5了。答案是否定的。
(2)为什么这么说呢?因为我上面说了,不同厂家对GPIO的命名是不同的,他们厂家的工程师最终会将这些GPIO抽象成一个引脚号。在这个抽象的过程中驱动中的命名可能会和原理图上的命名有些许出入,比如imax6ull开发板的GPIO5就是gpiochip4,因为imax6ull开发板是从GPIO1开始进行计算,而驱动程序中,是从gipo0开始的。
(3)如何确定是这样的呢?首先我们看上图,指导gpio0的地址为209c000,那么直接打开芯片手册,可以看到GPIO1的起始地址为209c000,正好对应

(4)现在知道GPIO5对应gpio4了,然后从终端中可以指导,gpio4的起始引脚号为128,那么GPIO5_3的引脚号就是128+3=131。

三、Linux的统一接口 — GPIO子系统

为什么需要统一接口

(1)讲解Linux的GPIO子系统之前,我先拿单片机开发做引子。
(2)对于绝大多数人而言,学习嵌入式开发,都是从51单片机开始的。STC89C52作为51单片机的一款经典单片机,大家多多少少都有些许了解。
(3)在编写STC89C52单片机的程序时候,我们都是直接对寄存器进行操作的,比如下面这个串口初始化程序。

void UartInit(void)		//9600bps@11.0592MHz
{
	SCON = 0x50;		//8位数据,可变波特率
	AUXR &= 0xBF;		//定时器1时钟为Fosc/12,即12T
	AUXR &= 0xFE;		//串口1选择定时器1为波特率发生器
	TMOD &= 0x0F;		//设定定时器1为16位自动重装方式
	TL1 = 0xE8;		//设定定时初值
	TH1 = 0xFF;		//设定定时初值
	ET1 = 0;		//禁止定时器1中断
	TR1 = 1;		//启动定时器1
}

(4)学习完51单片机之后,大多数人开始进阶STM32F103这款芯片。因为STM32F103这款芯片的寄存器很多,直接使用寄存器开发,查手册会相当的麻烦。于是ST公司就封装了一些库,如下为GPIO操作部分的库函数。

(5)我们会发现,不同的芯片,他的库函数大概率是不一样的。假如我们编写了一个业务程序,在STM32上跑的好好的。如果因为某些事情,我要换一款芯片,而这款芯片的库函数和STM32的不一样。最终会导致什么结果?很明显,所有业务程序需要重写编写!这是非常麻烦的事情!
(6)为了防止出现这种情况,Linux规定了,不管你是啥芯片,你只要想跑Linux,就必须给我统一接口!管你什么厂家,你的芯片让GPIO设置为输出的函数,名字必须叫做int gpio_direction_output()!
(7)这样做,存在什么好处呢?显而易见,我们业务代码不需要更改了,如果我们想换一款芯片,只需要底层稍微的改动一下即可。这也是为什么有些人说的,没跑Linux,阶级分明,跑了Linux,众生平等。

四、GPIO子系统函数介绍

Linux的GPIO子系统中可以通过如下函数配置GPIO。

int gpio_request(unsigned gpio, const char *label);
void gpio_free(unsigned gpio);
int gpio_direction_input(unsigned gpio);
int gpio_direction_output(unsigned gpio, int value);
int gpio_get_value(unsigned gpio);
void  gpio_set_value(unsigned gpio, int value);

(1)gpio_request()

/****** 函数介绍 ******/
/* 作用 :  向Linux 内核中用于请求申请一个 GPIO 引脚
 * 传入参数 : 
     * gpio : 要请求的 GPIO 引脚号
     * label : 给GPIO起一个名字
 * 返回参数 :  如何返回0,表示申请GPIO成功。如果返回负数,表示申请GPIO出现错误
*/
int gpio_request(unsigned gpio, const char *label);
  • 作用: 向Linux 内核中用于请求申请一个 GPIO 引脚的函数。如果我们想对一个引脚进行操作,需要最先调用 gpio_request()这个函数。
  • gpio : 要请求的 GPIO 引脚号。这个引脚号可以自己直接给出,还可以通过 of_get_named_gpio 函数从设备树获取指定 GPIO 属性信息(设备树的内容)
  • label : 给GPIO起一个名字,因为直接一个引脚号不方便人阅读,所以可以给这个引脚号起一个名字。随便起名字,只要你自己喜欢,不影响。
  • 返回值 : 如何返回0,表示申请GPIO成功。如果返回负数,表示申请GPIO出现错误。

(2)gpio_free()

/****** 函数介绍 ******/
/* 作用 : 如果不使用某个GPIO了,那么就需要调用 gpio_free 函数进行释放
 * 传入参数 : 
     * gpio : 要释放的GPIO引脚号
 * 返回参数 :  无
*/
void gpio_free(unsigned gpio);
  • 作用 : 如果不使用某个 GPIO 了, 那么就需要调用 gpio_free 函数进行释放。
  • gpio : 要释放的GPIO引脚号。与gpio_request的GPIO引脚号是同一个东西。
  • 返回参数 : 无

(3)gpio_direction_input()

/****** 函数介绍 ******/
/* 作用 : 设置某个 GPIO 为输入
 * 传入参数 : 
     * gpio : 要设置为输入的GPIO 引脚号
 * 返回参数 : 设置成功返回 0; 设置失败返回负值
*/
int gpio_direction_input(unsigned gpio);
  • 作用 : 将GPIO配置为输入方向。申请完GPIO之后,需要根据需求配置为输入或者输出,这个函数可以将GPIO设置为输入
  • gpio : 要设置为输入的GPIO 引脚号
  • 返回参数 : 返回 0,表示成功将 GPIO 引脚设置为输入模式。返回负数,表示出错或无法设置 GPIO 引脚。

(4)gpio_direction_output()

/****** 函数介绍 ******/
/* 作用 : 设置某个 GPIO 为输出,并且设置默认输出值
 * 传入参数 : 
     * gpio : 要设置为输出的GPIO 引脚号
     * value : GPIO 默认输出值
 * 返回参数 : 设置成功返回 0; 设置失败返回负值
*/
int gpio_direction_output(unsigned gpio, int value);
  • 作用 : 将GPIO配置为输出方向,并且设置默认输出值。申请完GPIO之后,需要根据需求配置为输入或者输出,这个函数可以将GPIO设置为输出
  • gpio : 设置为输出的GPIO 引脚号
  • value : GPIO 默认输出值。如果GPIO初始化成功之后,默认输出的电压。
  • 返回参数 : 返回 0,表示成功将 GPIO 引脚设置为输出模式。返回负数,表示出错或无法设置 GPIO 引脚。

(5)gpio_get_value()

/****** 函数介绍 ******/
/* 作用 : 获取指定GPIO的电平值
 * 传入参数 : 
     * gpio : 要获取电平值的GPIO标号
 * 返回参数 : 获取电平信息成功,高电平返回1,低电平返回0。GPIO电平获取失败返回负值
*/
int gpio_get_value(unsigned gpio);
  • 作用 : 获取指定GPIO的电平信息
  • gpio : 要获取电平值的GPIO标号
  • 返回参数 : 获取电平信息成功,高电平返回1,低电平返回0。GPIO电平获取失败返回负值。

(6)gpio_set_value()

  • 作用 : 设置指定GPIO的电平值
  • gpio : 要设置指定GPIO的电平值
  • value : 要设置的电平值,如果传入0,则表示将GPIO设置为低电平。传入一个非0值,表示将GPIO设置为高电平
  • 返回参数 : 无
/****** 函数介绍 ******/
/* 作用 : 获取指定GPIO的电平值
 * 传入参数 : 
     * gpio : 要设置指定GPIO的电平值
     * value : 要获取电平值的GPIO标号
 * 返回参数 : 无
*/
void  gpio_set_value(unsigned gpio, int value);

五、LED驱动代码

驱动层代码:

这里只展现代码,详细介绍参考大佬博客 Linux驱动入门 —— LED点灯驱动程序-CSDN博客

led_drv.c

#include "asm-generic/errno-base.h"
#include "asm-generic/gpio.h"
#include "asm/uaccess.h"
#include <linux/module.h>
#include <linux/poll.h>

#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/fcntl.h>
#include <linux/timer.h>

// 描述一个引脚
struct gpio_desc{
	int gpio;   // 引脚编号
    char *name; // 名字
};

static struct gpio_desc gpios[] = {
    {131, "led0", },  // 引脚编号,名字
};

/* 1. 确定主设备号                                                                 */
static int major = 0;
static struct class *gpio_class;  // 一个类,用于创建设备节点


/* 3. 实现对应的open/read/write等函数,填入file_operations结构体                     */
static ssize_t gpio_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	char tmp_buf[2];  // 存放驱动层和应用层交互的信息
	int err;   // 没有使用,用于存放copy_from_user和copy_to_user的返回值,消除报错
  int count = sizeof(gpios)/sizeof(gpios[0]); // 记录定义的最大引脚数量

	// 应用程序读的时候,传入的值如果不是两个,那么返回一个错误
	if (size != 2)
		return -EINVAL;
	
	/* 作用 : 驱动层得到应用层数据
	 * tmp_buf : 驱动层数据
	 * buf : 应用层数据
	 * 1  :数据长度为1个字节(因为我只需要知道他控制的是那一盏灯,所以只需要传入一个字节数据)
	*/
	err = copy_from_user(tmp_buf, buf, 1);
	
	//第0项表示要操作哪一个LED,如果操作的LED超出,表示失败
	if (tmp_buf[0] >= count)
		return -EINVAL;
	
	//将引脚电平读取出来
	tmp_buf[1] = gpio_get_value(gpios[(int)tmp_buf[0]].gpio);
	
	/* 作用 : 驱动层发数据给应用层
	 * buf : 应用层数据
	 * tmp_buf : 驱动层数据
	 * 2  :数据长度为2个字节
	*/
	err = copy_to_user(buf, tmp_buf, 2);
	
	return 2;
}

static ssize_t gpio_drv_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
    unsigned char ker_buf[2];
    int err;
	// 应用程序读的时候,传入的值如果不是两个,那么返回一个错误
    if (size != 2)
        return -EINVAL;
	
	/* 作用 : 驱动层得到应用层数据
	 * tmp_buf : 驱动层数据
	 * buf : 应用层数据
	 * size  :数据长度为size个字节
	*/
    err = copy_from_user(ker_buf, buf, size);

	// 如果要操作的GPIO不在规定范围内,返回错误
    if (ker_buf[0] >= sizeof(gpios)/sizeof(gpios[0]))
        return -EINVAL;

	// 设置指定引脚电平
    gpio_set_value(gpios[ker_buf[0]].gpio, ker_buf[1]);
    return 2;    
}



/* 2. 定义自己的file_operations结构体                                            */
static struct file_operations gpio_led_drv= {
	.owner	 = THIS_MODULE,
	.read    = gpio_drv_read,
	.write   = gpio_drv_write,
};



/* 4. 把file_operations结构体告诉内核:注册驱动程序                           */
/* 5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 */
static int __init gpio_drv_init(void)
{
    int err;  //用于保存函数返回值,用于判断函数是否执行成功
    int i;    //因为存在多个GPIO可能要申请,所以建立一个i进行for循环
    int count = sizeof(gpios)/sizeof(gpios[0]);  //统计有多少个GPIO
    
	/*__FILE__ :表示文件
	 *__FUNCTION__ :当前函数名
	 *__LINE__ :在文件的哪一行
	*/
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	for (i = 0; i < count; i++)
	{		
		/* 设置为输出引脚 */
		//申请指定GPIO引脚,申请的时候需要用到名字
		err = gpio_request(gpios[i].gpio, gpios[i].name);
		// 如果返回值小于0,表示申请失败
		if (err < 0) 
		{
			//如果GPIO申请失败,打印出是哪个GPIO申请出现问题
			printk("can not request gpio %s %d\n", gpios[i].name, gpios[i].gpio);
			return -ENODEV;
		}
		// 如果GPIO申请成功,设置输出高电平
		gpio_direction_output(gpios[i].gpio, 1);
	}

	/* 注册file_operations 	*/
	// 注册字符驱动程序
	major = register_chrdev(0, "100ask_led", &gpio_led_drv);  /* /dev/gpio_desc */
	
	/******这里相当于命令行输入 mknod  /dev/100ask_gpio c 240 0 创建设备节点*****/
	
	// 创建类,为THIS_MODULE模块创建一个类,这个类叫做gpio_class
	gpio_class = class_create(THIS_MODULE, "100ask_led_class");
	if (IS_ERR(gpio_class))   //如果返回错误
	{
		/*__FILE__ :表示文件
		 *__FUNCTION__ :当前函数名
		 *__LINE__ :在文件的哪一行
		*/
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		// 注销字符驱动程序
		unregister_chrdev(major, "100ask_led_class");
		// 返回错误
		return PTR_ERR(gpio_class);
	}
	
	/*输入参数是逻辑设备的设备名,即在目录/dev目录下创建的设备名
	 *参数一 : 在gpio_class类下面创建设备
	 *参数二 : 无父设备的指针
	 *参数三 : 主设备号+次设备号
	 *参数四 : 没有私有数据
	*/
	device_create(gpio_class, NULL, MKDEV(major, 0), NULL, "100ask_led"); /* /dev/100ask_gpio */
	
	// 如果执行到这里了,说明LED驱动装载完成
	printk("LED driver loading is complete\n");
	return err;
}

/* 6. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数 */
static void __exit gpio_drv_exit(void)
{
    int i;
    int count = sizeof(gpios)/sizeof(gpios[0]);
	/*__FILE__ :表示文件
	 *__FUNCTION__ :当前函数名
	 *__LINE__ :在文件的哪一行
	*/
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	// 销毁gpio_class类下面的设备节点
	device_destroy(gpio_class, MKDEV(major, 0));
	// 销毁gpio_class类
	class_destroy(gpio_class);
	// 注销驱动
	unregister_chrdev(major, "100ask_led");

	for (i = 0; i < count; i++)
	{
		// 将GPIO释放
		gpio_free(gpios[i].gpio);		
	}
	
	// 如果执行到这里了,说明LED驱动卸载完成
	printk("The LED driver is uninstalled\n");
}


/* 7. 其他完善:提供设备信息,自动创建设备节点                                     */

module_init(gpio_drv_init);     // 确认入口函数
module_exit(gpio_drv_exit);     // 确认出口函数

/*最后我们需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加
 *这个协议要求我们代码必须免费开源,Linux遵循GPL协议,他的源代码可以开放使用,那么你写的内核驱动程序也要遵循GPL协议才能使用内核函数
 *因为指定了这个协议,你的代码也需要开放给别人免费使用,同时可以根据这个协议要求很多厂商提供源代码
 *但是很多厂商为了规避这个协议,驱动源代码很简单,复杂的东西放在应用层
*/
MODULE_LICENSE("GPL"); // 指定模块为GPL协议
MODULE_AUTHOR("CSDN:qq_919426896");  // 表明作者,可以不写

应用层代码:

strtol()函数是将字符转换为数字。

因为我们在命令行中输入的1,其实是字符1,而不是数字1。为了和驱动层统一数据类型,所以这里需要调用这个函数。

ledtest.c

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

static int fd;

// int led_on(int which);
// int led_off(int which);
// int led_status(int which);

/* 可执行文件名   | 表示要操作哪一盏灯  | 灯状态  |    效果
 * ./led_test    |   <0|1|2|..>        | on     |硬件上开灯
 * ./led_test    |   <0|1|2|..>        | off    |硬件上关灯
 * ./led_test    |   <0|1|2|..>        |        |读取led状态,并且显示在终端
 */
int main(int argc, char **argv)
{
	int ret;     // 存放函数返回值,用于判断函数是否正常执行
	char buf[2]; // 存放命令行的后两个字符(<0|1|2|...> [on | off])

	
	// 如果传入参数少于两个,打印文件用法
	if (argc < 2) 
	{
		printf("Usage: %s <0|1|2|...> [on | off]\n", argv[0]);
		return -1;
	}


	// 打开文件,因为在驱动层中,device_create()函数创建的设备节点名字叫做100ask_led,而设备节点都存放在/dev目录下,所以这里是/dev/100ask_led
	fd = open("/dev/100ask_led", O_RDWR);
	// 如果无法打开,返回错误
	if (fd == -1)
	{
		printf("can not open file /dev/100ask_led\n");
		return -1;
	}
	// 如果传入了三个参数,表示写入
	if (argc == 3)
	{
		/* write */
		/* 作用 : 将字符串转化为一个整数
		 * argv[1] :  要转换为长整数的字符串
		 * NULL :如果提供了 endptr 参数,则将指向解析结束位置的指针存储在 endptr 中。endptr 可以用于进一步处理字符串中的其他内容
		 * 0 : 设置为 0,则会根据字符串的前缀(如 "0x" 表示十六进制,"0" 表示八进制,没有前缀表示十进制)来自动判断进制
		*/
		buf[0] = strtol(argv[1], NULL, 0);

		// 判断是否为打开
		if (strcmp(argv[2], "on") == 0)
			buf[1] = 0;  //因为LED外接3.3V,所以输出低电平才是开灯
		else
			buf[1] = 1;  //因为LED外接3.3V,所以输出高电平才是关灯
		// 向字符驱动程序中写入
		ret = write(fd, buf, 2);
	}
	// 否则表示读取电平信息
	else
	{
		/* read */
		/* 作用 : 将字符串转化为一个整数
		 * argv[1] :  要转换为长整数的字符串
		 * NULL :指向第一个不可转换的字符位置的指针
		 * 0 : 表示默认采用 10 进制转换
		*/
		buf[0] = strtol(argv[1], NULL, 0);
		// 读取电平,从驱动层读取两个数据
		ret = read(fd, buf, 2);
		// 如果返回值为2,表示正常读取到了电平。(为什么是2,看驱动程序的gpio_drv_read)
		if (ret == 2)
		{
			//打印引脚信息
			printf("led %d status is %s\n", buf[0], buf[1] == 0 ? "on" : "off");
		}
	}
	
	close(fd);
	
	return 0;
}

Makefile

KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
 
all:
	make -C $(KERN_DIR) M=`pwd` modules 
	$(CROSS_COMPILE)gcc -o ledtest ledtest.c 
 
clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order
	rm -f ledtest
 
obj-m	+= led_drv.o

上机测试:

可参考上一篇博客 Linux驱动入门 —— LED点灯驱动程序-CSDN博客

在ubuntu上执行make

挂载 NFS 目录,参考 开发板挂载 Ubuntu 的 NFS 目录-CSDN博客

打开开发板,这里使用的是imx6ull  

执行 insmod led_drv.ko 

开始测试

执行 ./ledtest 0 on  使开发板亮灯

测试结束执行 rmmod led_drv.ko 卸载驱动

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/243041.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

STM32的看门狗(WDG)

WDG&#xff08;Watchdog&#xff09;看门狗 看门狗可以监控程序的运行状态&#xff0c;当程序因为设计漏洞、硬件故障、电磁干扰等原因&#xff0c;出现卡死或跑飞现象时&#xff0c;看门狗能及时复位程序&#xff0c;避免程序陷入长时间的罢工状态&#xff0c;保证系统的可靠…

基于C/C++的rapidxml加载xml大文件 - 下部分

下载地址: RapidXml (sourceforge.net)https://rapidxml.sourceforge.net/ 将源码添加到自己的工程中 示例测试大文件耗时: 总共293w行数据&#xff0c;大概耗时不到1s。

Paper Reading: (U2PL) 基于不可靠伪标签的半监督语义分割

目录 简介目标/动机方法Pseudo-LabelingUsing Unreliable Pseudo-Labels 补充知识InfoNCE LossOHEM 实验Comparison with Existing AlternativesAblationEffectiveness of Using Unreliable Pseudo-LabelsAlternative of Contrastive Learning 总结附录U2PL 与 negative learni…

【C语言程序设计】数组程序设计

目录 前言 一、数组的定义和初始化 二、数组的基本操作 三、数组的高级应用 四、程序设计 4.1 程序设计第一题 4.2 程序设计第二题 4.3 程序设计第三题 总结 &#x1f308;嗨&#xff01;我是Filotimo__&#x1f308;。很高兴与大家相识&#xff0c;希望我的博客能对你有所帮助…

论文阅读《DPS-Net: Deep Polarimetric Stereo Depth Estimation》

论文地址&#xff1a;https://openaccess.thecvf.com/content/ICCV2023/html/Tian_DPS-Net_Deep_Polarimetric_Stereo_Depth_Estimation_ICCV_2023_paper.html 概述 立体匹配模型难以处理无纹理场景的匹配&#xff0c;现有的方法通常假设物体表面是光滑的&#xff0c;或者光照是…

设计模式(2)--对象创建(4)--原型

1. 意图 用原型实例指定创建对象的种类&#xff0c;并且通过拷贝这些原型创建新的对象。 2. 两种角色 抽象原型(Prototype)、具体原型(Concrete Prototype) 3. 优点 3.1 对客户隐藏了具体的产品类 3.2 可以在运行时刻增加和删除产品 3.3 可以极大地减少系统所需要的类的数目 …

Weblogic-CVE-2023-21839

一、漏洞概述 RCE漏洞&#xff0c;该漏洞允许未经身份验证的远程&#xff0c;通过T3/IIOP协议网络访问并破坏WebLogic服务器&#xff0c;成功利用此漏洞可导致Oracle WebLogic服务器被接管&#xff0c;通过rmi/ldap远程协议进行远程命令执行,当 JDK 版本过低或本地存在小工具&…

@Scheduled任务调度/定时任务-非分布式

1、功能概述 任务调度就是在规定的时间内执行的任务或者按照固定的频率执行的任务。是非常常见的功能之一。常见的有JDK原生的Timer, ScheduledThreadPoolExecutor以及springboot提供的Schduled。分布式调度框架如QuartZ、Elasticjob、XXL-JOB、SchedulerX、PowerJob等。 本文…

出现 ‘mvn‘ 不是内部或外部命令,也不是可运行的程序或批处理文件 的解决方法

目录 1. 问题所示2. 原理分析3. 解决方法1. 问题所示 下载了Maven,也配置了环境,在环境变量中配置MAVEN_HOME,在用户变量中配置了bin变量 具体如下所示: 用户变量的配置: 结果显示如下所示: 2. 原理分析 HOME变量中会具体到jre变量,如果在用户变量中配置,jre可能…

SpringBoot+Vue3前后端快速整合入门

前言 最近需要维护一个个人项目&#xff0c;由于笔者是一个大后端&#xff0c;所以借此机会把前端学习过程记录一下&#xff0c;方便后续回顾。 前端项目初始化 安装npm 在前端项目初始化时&#xff0c;我们必须要安装好node&#xff0c;官网地址如下&#xff0c;因为笔者后…

数据结构实验任务八:排序算法的实现与分析

问题描述 统计成绩&#xff1a;给出 n 个学生的考试成绩表&#xff0c;每条信息由姓名和分数组成&#xff0c;试设 计一个算法&#xff1a; 1.按分数高低次序&#xff0c;打印出每个学生在考试中获得的名次&#xff0c;分数相同的为同 一名次&#xff1b; 2.按名次列出每个学生…

36V/48V转12V 10A直流降压DC-DC芯片-AH1007

AH1007是一款36V/48V转12V 10A直流降压&#xff08;DC-DC&#xff09;芯片&#xff0c;它是一种高性能的降压变换器&#xff0c;常用于工业、汽车和电子设备等领域。 AH1007采用了先进的PWM调制技术和开关电源控制算法&#xff0c;能够高效地将输入电压从36V/48V降低到12V&…

【Spark精讲】Spark内存管理

目录 前言 Java内存管理 Java运行时数据区 Java堆 垃圾回收机制 Executor内存管理 内存类型 堆内内存 堆外内存 内存管理模式 静态内存管理 统一内存管理 ​编辑 执行内存管理 多任务间内存分配 Shuffle 的内存占用 MemoryOverHead详解 任务内存调节 错误类型…

TCP/IP 四层体系结构

目录 ​编辑 导言&#xff1a; 应用层 1. HTTP&#xff08;超文本传输协议&#xff09; 2. FTP&#xff08;文件传输协议&#xff09; 3. SMTP&#xff08;简单邮件传输协议&#xff09; 4. POP3&#xff08;邮局协议第3版&#xff09; 5. IMAP&#xff08;互联网消息访…

群晖(Synology)新建存储池使用 Home 服务

每一个用户都可以有一个自己的 Home 服务。 这个在群晖存储新建存储池后可以自动启用这个服务。 启用后&#xff0c;可以看到你的文件系统中有一个 homes 的文件了。 群晖&#xff08;Synology&#xff09;新建存储池使用 Home 服务 - 系统容器 - iSharkFly每一个用户都可以有…

尚硅谷JavaWeb电子书城项目(Java+Mysql+Tomcat+Jsp)

自己写的在线电子书城项目&#xff0c;可改写&#xff0c;添加功能&#xff0c;如打折&#xff0c;分类&#xff0c;用户管理&#xff0c;评论等功能。 使用方法&#xff1a; 1.使用idea导入项目。 2.数据库要用项目resource文件里的book.sql文件建立。 3.修改jdbc.properi…

【从零开始学习JVM | 第八篇】学习垃圾回收算法 和 垃圾回收器

前言&#xff1a; 现代编程语言通常采用垃圾回收机制来自动管理内存。垃圾回收机制是一种自动化的内存管理技术&#xff0c;可以在程序运行时自动识别和回收不再使用的内存&#xff0c;从而减少内存泄漏和其他内存相关问题的发生。 本文将介绍垃圾回收算法和垃圾回收器的相关…

【产品】Axure的基本使用(二)

文章目录 一、元件基本介绍1.1 概述1.2 元件操作1.3 热区的使用 二、表单型元件的使用2.1 文本框2.2 文本域2.3 下拉列表2.4 列表框2.5 单选按钮2.6 复选框2.7 菜单与表格元件的使用 三、实例3.1 登录2.2 个人简历 一、元件基本介绍 1.1 概述 在Axure RP中&#xff0c;元件是…

模块四(一):搭建自己的SSR

前言&#xff1a;同构渲染是将服务器渲染和客户端渲染相结合的一种渲染方式&#xff0c;在服务端生成初始页面&#xff0c;提升首屏加载速度&#xff0c;并且有利于SEO&#xff1b;在客户端接管HTML&#xff0c;并且将静态HTML激活为数据绑定的动态HTML&#xff0c;为用户提供更…

算法通关村第五关—LRU的设计与实现(黄金)

LRU的设计与实现 一、理解LRU的原理 LeetCode146:运用你所掌握的数据结构&#xff0c;设计和实现一个LRU(最近最少使用)缓存机制 实现LRUCache类&#xff1a; LRUCache(int capacity) 以正整数作为容量capacity初始化 LRU 缓存 int get(int key) 如果关键字key存在于缓存中&a…