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

news2024/12/25 15:50:08

目录

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

编写驱动程序的步骤:

对于 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.coloradmin.cn/o/1308690.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于自动化脚本批量上传依赖到nexus内网私服

前言 因为某些原因某些企业希望私服是不能连接外网的&#xff0c;所以需要某些开源依赖需要我们手动导入到nexus中&#xff0c;尽管nexus为我们提供了web页面。但是一个个手动导入显然是一个庞大的工程。 对此我们就不妨基于脚本的方式实现这一过程。 预期效果 笔者本地仓库…

【Java】网络编程-UDP回响服务器客户端简单代码编写

这一篇文章我们将讲述网络编程中UDP服务器客户端的编程代码 1、前置知识 UDP协议全称是用户数据报协议&#xff0c;在网络中它与TCP协议一样用于处理数据包&#xff0c;是一种无连接的协议。 UDP的特点有&#xff1a;无连接、尽最大努力交付、面向报文、没有拥塞控制 本文讲…

为什么FPGA是战略芯片?

FPGA&#xff08;Field Programmable Gate Array&#xff09;是在PAL&#xff08;可编程阵列逻辑&#xff09;、GAL&#xff08;通用阵列逻辑&#xff09;等可编程器件的基础上进一步发展的产物&#xff0c;它是作为一种半定制电路而出现的&#xff0c;既解决了定制电路的不足&…

提升数据采集技能:用 Axios 实现的 Twitter 视频下载器全面解析

引入 在当今数据驱动的时代&#xff0c;高效的数据采集是实现成功数据科学项目的关键。数据采集不仅涉及到数据的获取&#xff0c;还包括数据的清洗、转换、存储和分析等多个环节。Twitter作为全球最大的社交媒体平台之一&#xff0c;蕴含着丰富的信息和海量的多媒体内容&…

计算机网络:数据链路层(网桥)

带你速通计算机网络期末 目录 一、冲突域和广播域 二、网桥介绍 三、网桥分类—―透明网桥 四、网桥分类―—源路由网桥 五、多接口网桥―—以太网交换机 总结 一、冲突域和广播域 冲突域:在同一个冲突域中的每一个节点都能收到所有被发送的帧。简单的说就是同一时间内只…

C# WPF上位机开发(内嵌虚拟机的软件开发)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 学习过halcon的同学都知道&#xff0c;它不仅有很多的图像算子可以使用&#xff0c;而且调试很方便。每一步骤的调试结果&#xff0c;都可以看到对…

WWW 指南-万维网联盟(World Wide Web)

WWW - 万维网联盟 WWW通常称为网络。 web是一个世界各地的计算机网络。 电脑在Web上使用标准语言沟通。 万维网联盟&#xff08;W3C&#xff09;制定了Web标准 什么是WWW&#xff1f; WWW 代表 World Wide Web(万维网)万维网常常被称为 网络网络是世界各地的计算机网络网络中…

Spark分布式内存计算框架

目录 一、Spark简介 &#xff08;一&#xff09;定义 &#xff08;二&#xff09;Spark和MapReduce区别 &#xff08;三&#xff09;Spark历史 &#xff08;四&#xff09;Spark特点 二、Spark生态系统 三、Spark运行架构 &#xff08;一&#xff09;基本概念 &#x…

博客社区资讯APP源码/开源知识付费社区小程序源码/资源社区源码/独有付费阅读+兼容安卓苹果

源码简介&#xff1a; 博客社区资讯APP源码&#xff0c;它是开源知识付费小程序源码&#xff0c;作为资源社区源码&#xff0c;它具有独有付费阅读兼容安卓苹果。它是Typecho后端的。 知识付费社区RuleApp多内容发布&#xff0c;后端基于Typoche博客程序开发带完整安装文档 竟…

互联网,我们的虚拟世界

同学们&#xff0c;你们知道互联网是干什么的吗&#xff1f;它就像一个虚拟的世界&#xff0c;让我们能够连接到任何地方&#xff0c;获取任何信息&#xff0c;就像你现在正在通过互联网阅读我的文章一样。 互联网 你们有没有想过&#xff0c;如果没有互联网&#xff0c;我们的…

借助3D文档控件Aspose.3D,用Java 创建 3D 场景

3D 场景是在计算机上显示 3D 形状的一种方式。在本指南中&#xff0c;我们将学习如何使用 Java 创建 3D 场景&#xff0c;而不需要任何特殊的 3D 软件。之后&#xff0c;我们将以FBX文件格式保存 3D 场景&#xff0c;这是共享 3D 内容的常见方式。那么&#xff0c;让我们开始吧…

BearPi Std 板从入门到放弃 - 先天神魂篇(3)(RT-Thread I2C设备)

简介 使用BearPi IOT Std开发板及其扩展板E53_SC1&#xff0c; SC1上有I2C1 的光照强度传感器BH1750 和 EEPROM AT24C02&#xff0c; 本次主要就是读取光照强度; 主板: 主芯片: STM32L431RCT6LED : PC13 \ 推挽输出\ 高电平点亮串口: Usart1I2C使用 : I2C1E53_SC1扩展板 : LE…

4G工业路由器物联网解决方案智慧储能系统

储能系统是用于电网和用户间起到电力缓冲和削峰填谷作用的电力管理平台。储能系统通常由电池、充电机、控制器、电能质量治理装置及监控系统组成。主要应用于可再生能源发电系统&#xff0c;电力需求侧响应&#xff0c;电动汽车充电等领域。 4G工业路由器是一款专门针对物联网…

winform使用CefSharp嵌入VUE网页并交互

1、NuGet添加CefSharp 如果下载慢或失败可以更新下载源 腾讯资源https://mirrors.cloud.tencent.com/nuget/华为资源https://repo.huaweicloud.com/repository/nuget/v3/index.json 2、将项目平台改为X64 3、在winform窗体添加cef using CefSharp; using CefSharp.WinForms; u…

Flink的容错机制

容错机制 容错&#xff1a;指出错后不影响数据的继续处理&#xff0c;并且恢复到出错前的状态。 检查点&#xff1a;用存档读档的方式&#xff0c;将之前的某个时间点的所有状态保存下来&#xff0c;故障恢复继续处理的结果应该和发送故障前完全一致&#xff0c;这就是所谓的检…

【C语言】操作符详解(三)

目录 逗号表达式 下标访问[ ]&#xff0c;函数调用&#xff08; &#xff09; 下标引用操作符[ ] 函数调用操作符&#xff08;&#xff09; 逗号表达式 exp1&#xff0c;exp2&#xff0c;exp3&#xff0c;...expN 逗号表达式&#xff0c;就是用逗号隔开的多个表达式…

C# 提取PDF中指定文本、图片的坐标

获取PDF文件中文字或图片的坐标可以实现精确定位&#xff0c;这对于快速提取指定区域的元素&#xff0c;以及在PDF中添加注释、标记或自动盖章等操作非常有用。本文将详解如何使用国产PDF库通过C# 提取PDF中指定文本或图片的坐标位置&#xff08;X, Y轴&#xff09;。 ✍ 用于…

Linux---切换目录命令

1. 切换目录命令的使用 命令说明cd 目录切换到指定目录cd ~切换到当前用户的主目录cd ..切换到上一级目录cd .切换到当前目录cd -切换到上一次目录 注意: cd命令切换目录时&#xff0c;这个目录必须存在。cd 后面不写目录等价于cd ~ cd 目录效果图: cd ~效果图: cd ..效果图…

leetcode --15 三数之和 【双指针 C++】

原题链接&#xff1a;15. 三数之和 - 力扣&#xff08;LeetCode&#xff09; 题目解析&#xff1a; 题目中说的不可以包含重复的三元组&#xff0c;从示例1可以看出[-1,0,1] 和[0,1,-1]虽然三个数顺序不同但是元素重复了&#xff0c;所以只选取其中一个。而本题难点也在于去重…

论文阅读:MonetDB/X100: Hyper-Pipelining Query Execution

目录 Abstract 1 Introduction 1.1 Outline 2 How CPU Work Abstract 在决策支持、OLAP和多媒体检索等计算密集型应用领域&#xff0c;数据库系统往往只能在现代cpu上实现较低的IPC(每周期指令)效率。本文首先以TPC-H基准为重点&#xff0c;深入研究了这种情况发生的原因。…