第九节 使用设备树实现RGB 灯驱动

news2024/11/27 21:33:29

通过上一小节的学习,我们已经能够编写简单的设备树节点,并且使用常用的of 函数从设备树中获取我们想要的节点资源。这一小节我们带领大家使用设备树编写一个简单的RGB 灯驱动程序,加深对设备树的理解。

实验说明

本节实验使用到STM32MP1 开发板上的RGB 彩灯

硬件原理图分析

参考”字符设备驱动–点亮LED 灯”章节

实验代码讲解

本章的示例代码目录为:linux_driver/device_tree_rgb_led

编程思路

程序编写的主要内容为添加RGB 灯的设备树节点、在驱动程序中使用of 函数获取设备节点中的属性,编写测试应用程序。

  • 首先向设备树添加RGB 设备节点
  • 其次编写平台设备驱动框架,主要包驱动入口函数、驱动注销函数、平台设备结构体定义三部分内容。
  • 实现.probe 函数,对rgb 进行设备注册和初始化。
  • 实现字符设备操作函数集,这里主要实现.write 操作。
  • 编写测试应用程序,对于输入不同的值控制rgb 颜色。

代码分析

添加RGB 设备节点

RGB 灯实际使用的是一个IO 口,控制它所需要的资源几个控制寄存器,所以它的设备树节点也非常简单如下所示。

列表1: 添加RGB 设备节点

/* 添加led 节点*/
rgb_led{
	#address-cells = <1>;
	#size-cells = <1>;
	compatible = "fire,rgb_led";

	/* 红灯节点*/
	ranges;
	rgb_led_red@0x50002000{
		compatible = "fire,rgb_led_red";
		reg = < 0x50002000 0x00000004
				0x50002004 0x00000004
				0x50002008 0x00000004
				0x5000200C 0x00000004
				0x50002018 0x00000004
				0x50000A28 0x00000004>;
		status = "okay";
	};

	/* 绿灯节点*/
	rgb_led_green@0x50000A28{
		compatible = "fire,rgb_led_green";
		reg = < 0x50008000 0x00000004
				0x50008004 0x00000004
				0x50008008 0x00000004
				0x5000800C 0x00000004
				0x50008018 0x00000004
				0x50000A28 0x00000004>;
		status = "okay";
	};

	/* 蓝灯节点*/
	rgb_led_blue@0x50000A28{
		compatible = "fire,rgb_led_blue";
		reg = < 0x50003000 0x00000004
				0x50003004 0x00000004
				0x50003008 0x00000004
				0x5000300C 0x00000004
				0x50003018 0x00000004
				0x50000A28 0x00000004>;
	status = "okay";
};

RGB 灯的设备节点添加到了根节点的末尾, 完整内容请参考本章配套代码
linux_driver/device_tree_rgb_led/stm32mp157a-basic.dts。

上面添加的设备树中,代码里包含了控制RGB 灯的三个引脚所使用的的寄存器,这些寄存器的作用以及用法已经在裸机部分详细介绍,这里不再赘述,如有疑问可以参考字符设备驱动——点亮LED 灯章节。

  • 第2-42 行:这里就是RGB 灯的设备树节点,节点名“rgb_led”由于在根节点下,很明显它的设备树路径为“/rgb_led”, 在驱动程序中我们会用到这cells”定义了它的子节点的reg 属性样式。“compatible”属性用于匹配驱动,在驱动我们会配置一个和“compatible”一样的参数,这样加载驱动是就可以自动和这个设备树节点匹配了。

  • 第9-41 行: rgb_led 节点的子节点。RGB 灯使用了三个引脚,如上所示,它会用到16 个寄存器,为方便管理,我们为每个引脚创建了一个子节点,从上到下依次为红灯控制引脚、绿灯控制引脚、蓝灯控制引脚。它们三个非常相似,我们这里只以第一个红灯控制引脚为例讲解。在红灯子节点中只定义了三个属性,“compatie = “fire,rgb_led_red””表示这是一个红灯子节点,对于本实验来说可有可无。“reg = < ⋯>”定义红灯引脚使用到寄存器。一共有五个,排列顺序与注释中的一致。“status = “okay””定义子节点的状态,我们要用这个子节点所以设置为“okay”。

编写驱动程序

基于设备树的驱动程序与平台总线驱动非常相似,差别是平台总线驱动中的平台驱动要和平台设备进行匹配,使用设备树后设备树取代“平台设备”的作用,平台驱动只需要和与之对应的设备树节点匹配即可。

驱动程序主要内容包括编写平台设备驱动框架、编写.prob 函数、实现字符设备操作函数集、驱动注销四部分内容。源linux_driver/device_tree_rgb_led/rgb_led.c。

驱动入口函数

驱动入口函数仅仅注册一个平台驱动,如下所示

列表2: 驱动初始化函数

/*
* 驱动初始化函数
*/
static int __init led_platform_driver_init(void)
{
	int DriverState;
	DriverState = platform_driver_register(&led_platform_driver);
	printk(KERN_EMERG "\tDriverState is %d\n", DriverState);
	return 0;
}

在整个入口函数中仅仅调用了“platform_driver_register”函数注册了一个平台驱动。参数是传入一个平台设备结构体。

定义平台设备结构体

注册平台驱动时会用到平台设备结构体,在平台设备结构体主要作用是指定平台驱动的.probe 函数、指定与平台驱动匹配的平台设备,使用了设备树后就是指定与平台驱动匹配的设备树节点。

列表3: 平台设备结构体

static const struct of_device_id rgb_led[] = {
	{.compatible = "fire,rgb_led"},
	{/* sentinel */}
};

/* 定义平台设备结构体*/
struct platform_driver led_platform_driver = {
	.probe = led_probe,
	.driver = {
	.name = "rgb-leds-platform",
	.owner = THIS_MODULE,
	.of_match_table = rgb_led,
	}
	};
  • 第1-4 行:定义匹配表
  • 第7-8 行:就是我们定义的平台设备结构体。其中“.probe =led_probe,”指定.probe 函数。.probe 函数比较特殊,当平台驱动和设备树节点匹配后会自动执行.probe 函数,后面的RGB灯的初始化以及字符设备的注册都在这个函数中实现(当然也可以在其他函数中实现)。
  • 第9-14 行:“.driver = { ⋯}”定义driver 的一些属性,包括名字、所有者等等,其中最需要注意的是“.of_match_table ”属性,它指定这个驱动的匹配表。这里只定义了一个匹配值“.compatible = “fire,rgb_led”,这个驱动将会和设备树中“compatible =“fire,rgb_led”的节点匹配”,准确的说是和““compatible = “fire,rgb_led””的相对根节点的子节点匹配。我们在根节点下定义了rgb_led 子节点,并且设置“compatible = “fire,rgb_led”; 所以正常情况下,驱动会和这个子节点匹配。

实现.probe 函数

之前说过,当驱动和设备树节点匹配成功后会自动执行.probe 函数,所以我们在.probe 函数中实现一些初始化工作。本实验将RGB 初始化以及字符设备的初始化全部放到.probe 函数中实现,.probe 函数较长,但包含大量的简单、重复性的初始化代码,非常容易理解。

列表4: .probe 函数

/* 定义led 资源结构体,保存获取得到的节点信息以及转换后的虚拟寄存器地址*/
struct led_resource
{
	struct device_node *device_node; //rgb_led_red 的设备树节点
	void __iomem *va_MODER;
	void __iomem *va_OTYPER;
	void __iomem *va_OSPEEDR;
	void __iomem *va_PUPDR;
	void __iomem *va_BSRR;
};

static void __iomem *va_clkaddr;

static int led_probe(struct platform_device *pdv)
{

	int ret = -1; //保存错误状态码
	unsigned int register_data = 0;

	printk(KERN_EMERG "\t match successed \n");

	/* 获取rgb_led 的设备树节点*/
	rgb_led_device_node = of_find_node_by_path("/rgb_led");
	if (rgb_led_device_node == NULL)
	{
		printk(KERN_ERR "\t get rgb_led failed! \n");
		return -1;
	}

	/* 获取rgb_led 节点的红灯子节点*/
	led_red.device_node = of_find_node_by_name(rgb_led_device_node,"rgb_led_red");
	if (led_red.device_node == NULL)
	{
		printk(KERN_ERR "\n get rgb_led_red_device_node failed ! \n");
return -1;
	}


	/* 获取reg 属性并转化为虚拟地址*/
	led_red.va_MODER = of_iomap(led_red.device_node, 0);
	led_red.va_OTYPER = of_iomap(led_red.device_node, 1);
	led_red.va_OSPEEDR = of_iomap(led_red.device_node, 2);
	led_red.va_PUPDR = of_iomap(led_red.device_node, 3);
	led_red.va_BSRR = of_iomap(led_red.device_node, 4);
	va_clkaddr = of_iomap(led_red.device_node, 5);

	register_data = readl(va_clkaddr);
	// 开启a、b、g 的时钟
	register_data |= (0x43); // 开启a、b、g 的时钟
	writel(register_data, va_clkaddr);

	// 设置模式寄存器:输出模式
	register_data = readl(led_red.va_MODER);
	register_data &= ~((unsigned int)0X3 << (2 * 13));
	register_data |= ((unsigned int)0X1 << (2 * 13));
	writel(register_data,led_red.va_MODER);
	// 设置输出类型寄存器:推挽模式
	register_data = readl(led_red.va_OTYPER);
	register_data &= ~((unsigned int)0X1 << 13);
	writel(register_data, led_red.va_OTYPER);
	// 设置输出速度寄存器:高速
	register_data = readl(led_red.va_OSPEEDR);
	register_data &= ~((unsigned int)0X3 << (2 * 13));
	register_data |= ((unsigned int)0x2 << (2 * 13));
	writel(register_data, led_red.va_OSPEEDR);
	// 设置上下拉寄存器:上拉
	register_data = readl(led_red.va_PUPDR);
	register_data &= ~((unsigned int)0X3 << (2*13));
	register_data |= ((unsigned int)0x1 << (2*13));
	writel(register_data,led_red.va_PUPDR);
	// 设置置位寄存器:默认输出高电平
	register_data = readl(led_red.va_BSRR);
	register_data |= ((unsigned int)0x1 << (13));
	writel(register_data, led_red.va_BSRR);

	/* 获取rgb_led 节点的绿灯子节点*/
	led_green.device_node = of_find_node_by_name(rgb_led_device_node,"rgb_led_green");
	if (led_green.device_node == NULL)
	{
		printk(KERN_ERR "\n get rgb_led_green_device_node failed ! \n");
		return -1;
	}

	/* 获取reg 属性并转化为虚拟地址*/
	led_green.va_MODER = of_iomap(led_green.device_node, 0);
	led_green.va_OTYPER = of_iomap(led_green.device_node, 1);
	led_green.va_OSPEEDR = of_iomap(led_green.device_node, 2);
	led_green.va_PUPDR = of_iomap(led_green.device_node, 3);
	led_green.va_BSRR = of_iomap(led_green.device_node, 4);
	// 设置模式寄存器:输出模式
	register_data = readl(led_green.va_MODER);
	register_data &= ~((unsigned int)0X3 << (2 * 2));
	register_data |= ((unsigned int)0X1 << (2 * 2));
	writel(register_data,led_green.va_MODER);
	// 设置输出类型寄存器:推挽模式
	register_data = readl(led_green.va_OTYPER);
	register_data &= ~((unsigned int)0X1 << 2);
	writel(register_data, led_green.va_OTYPER);
	// 设置输出速度寄存器:高速
	register_data = readl(led_green.va_OSPEEDR);
	register_data &= ~((unsigned int)0X3 << (2 * 2));
	register_data |= ((unsigned int)0x2 << (2 * 2));
	writel(register_data, led_green.va_OSPEEDR);
	// 设置上下拉寄存器:上拉
	register_data = readl(led_green.va_PUPDR);
	register_data &= ~((unsigned int)0X3 << (2*2));
	register_data |= ((unsigned int)0x1 << (2*2));
	writel(register_data,led_green.va_PUPDR);
	// 设置置位寄存器:默认输出高电平
	register_data = readl(led_green.va_BSRR);
	register_data |= ((unsigned int)0x1 << (2));
	writel(register_data, led_green.va_BSRR);

	/* 获取rgb_led 节点的蓝灯子节点*/
	led_blue.device_node = of_find_node_by_name(rgb_led_device_node,"rgb_led_blue");
	if (led_blue.device_node == NULL)
	{
		printk(KERN_ERR "\n get rgb_led_blue_device_node failed ! \n");
		return -1;
	}

	/* 获取reg 属性并转化为虚拟地址*/
	led_blue.va_MODER = of_iomap(led_blue.device_node, 0);
	led_blue.va_OTYPER = of_iomap(led_blue.device_node, 1);
	led_blue.va_OSPEEDR = of_iomap(led_blue.device_node, 2);
	led_blue.va_PUPDR = of_iomap(led_blue.device_node, 3);
	led_blue.va_BSRR = of_iomap(led_blue.device_node, 4);

	// 设置模式寄存器:输出模式
	register_data = readl(led_blue.va_MODER);
	register_data &= ~((unsigned int)0X3 << (2 * 5));
	register_data |= ((unsigned int)0X1 << (2 * 5));
	writel(register_data,led_blue.va_MODER);
	// 设置输出类型寄存器:推挽模式
	register_data = readl(led_blue.va_OTYPER);
	register_data &= ~((unsigned int)0X1 << 5);
	writel(register_data, led_blue.va_OTYPER);
	// 设置输出速度寄存器:高速
	register_data = readl(led_blue.va_OSPEEDR);
	register_data &= ~((unsigned int)0X3 << (2 * 5));
	register_data |= ((unsigned int)0x2 << (2 * 5));
	writel(register_data, led_blue.va_OSPEEDR);
	// 设置上下拉寄存器:上拉
	register_data = readl(led_blue.va_PUPDR);
	register_data &= ~((unsigned int)0X3 << (2*5));
	register_data |= ((unsigned int)0x1 << (2*5));
	writel(register_data,led_blue.va_PUPDR);
	// 设置置位寄存器:默认输出高电平
	register_data = readl(led_blue.va_BSRR);
	register_data |= ((unsigned int)0x1 << (5));
	writel(register_data, led_blue.va_BSRR);
	/*---------------------注册字符设备部分-----------------*/

	//第一步
	//采用动态分配的方式,获取设备编号,次设备号为0,
	//设备名称为rgb-leds,可通过命令cat /proc/devices 查看
	//DEV_CNT 为1,当前只申请一个设备编号
	ret = alloc_chrdev_region(&led_devno, 0, DEV_CNT, DEV_NAME);
	if (ret < 0)
	{
		printk("fail to alloc led_devno\n");
		goto alloc_err;
	}
	//第二步
	//关联字符设备结构体cdev 与文件操作结构体file_operations
	led_chr_dev.owner = THIS_MODULE;
	cdev_init(&led_chr_dev, &led_chr_dev_fops);
	//第三步
	//添加设备至cdev_map 散列表中
	ret = cdev_add(&led_chr_dev, led_devno, DEV_CNT);
	if (ret < 0)
	{
		printk("fail to add cdev\n");
		goto add_err;
	}

	//第四步
	/* 创建类*/
	class_led = class_create(THIS_MODULE, DEV_NAME);

	/* 创建设备*/
	device = device_create(class_led, NULL, led_devno, NULL, DEV_NAME);
	return 0;

	add_err:
	//添加设备失败时,需要注销设备号
	unregister_chrdev_region(led_devno, DEV_CNT);
	printk("\n error! \n");
	alloc_err:

	return -1;
}
  • 第2-12 行:自定义led 资源结构体,用于保存获取得到的设备节点信息以及转换后的虚拟寄存器地址。
  • 第23-27 行:使用of_find_node_by_path 函数获取设备树节点“/rgb_led”,获取成功后会返回“/rgb_led”节点的“设备节点结构体”后面的代码我们就可以根据这个“设备节点结构体”访问它的子节点。
  • 第31-152 行:依次初始化红、绿、蓝灯,这三部分非常相似,这里仅介绍第三部分红灯初始化部分。初始化过程如下:
  • 第31-36 行: 获取红灯子节点, 这里使用函数“of_find_node_by_name” , 参数rgb_led_device_node 指定从rgb_led 节点开始搜索,参数“rgb_led_red”指定要获取那个节点,这里是rgb_led 节点下的rgb_led_red 子节点。
  • 第40-45 行:获取并转换reg 属性,我们知道reg 属性保存的就是寄存器地址(物理地址),这里使用“of_iomap”函数,获取并完成物理地址到虚拟地址的转换。
  • 第47-74 行:初始化寄存器,至于如何将初始化GPIO 在字符设备-点亮LED 章节已经详细介绍这里不再赘述,需要注意的是这里只能用系统提供的API(例如这里读写的是32 位数据,使用writel 和readl),不能像裸机那样直接使用“=”、“&=”、“|=”等等那样直接修改寄存器。
  • 第160-184 行:注册一个字符设备。字符设备的注册过程与之前讲解的字符设备驱动非常相似,这部分代码就是从字符设备驱动拷贝得到的。这里仅仅做简单介绍。

列表5: 注册字符设备使用到的结构体

static dev_t led_devno; //定义字符设备的
设备号
static struct cdev led_chr_dev; //定义字符设备结构体chr_dev
struct class *class_led; //保存创建的类
struct device *device; // 保存创建的设备

static struct file_operations led_chr_dev_fops ={
	.owner = THIS_MODULE,
	.open = led_chr_dev_open,
	.write = led_chr_dev_write,
};
  • 第164-169 行:使用“alloc_chrdev_region”动态申请主设备号,并保存到led_devno 结构体中。
  • 第172-173 行:使用“cdev_init”初始化字符设别。
  • 第176-181 行:使用“cdev_add”将字符设备添加到系统。如果需要驱动自动创建设备节点,则还要创建类和设备。
  • 第184 行:使用“class_create”函数创建类。
  • 第187 行:使用“device_create”创建设备,其中参数“DEV_NAME”用于指定设备节点名,这个名字在应用程序中会用到。

如果驱动和设备树节点完成匹配,系统会自动执行.probe 函数,从上方代码可知,.probe 函数完成了RGB 灯的初始化和字符设备的创建。下一步我们只需要在字符设备的操作函数集中控制RGB灯即可。

实现字符设备操作函数集

为简化程序设计这里仅仅实现了字符设备操作函数集中的.write 函数,.write 函数根据收到的信息控制RGB 灯的亮、灭,结合代码介绍如下:

列表6: .write 函数实现

/* 字符设备操作函数集,open 函数*/
static int led_chr_dev_open(struct inode *inode, struct file *filp)
{
	printk("\n open form driver \n");
	return 0;
}

/* 字符设备操作函数集,write 函数*/
static ssize_t led_chr_dev_write(struct file *filp, const char __user *buf,size_t cnt, loff_t *offt)
{

	unsigned int register_data = 0; //暂存读取得到的寄存器数据
	unsigned char write_data; //用于保存接收到的数据

	int error = copy_from_user(&write_data, buf, cnt);
	if (error < 0)
	{
		return -1;
	}
	// 开启a、b、g 的时钟
	writel(0x43, va_clkaddr);
	/* 设置GPIOA13 输出电平*/
	if (write_data & 0x04)
	{
	register_data |= (0x01 << (13+16));
	writel(register_data, led_red.va_BSRR); // GPIOA13 引脚输出低电平,红灯}
	else
	{
		register_data |= (0x01 << (13));
		writel(register_data, led_red.va_BSRR); // GPIOA13 引脚输出高电平,红灯灭
	}

	/* 设置GPIOG2 输出电平*/
	if (write_data & 0x02)
	{
		register_data |= (0x01 << (2+16));
		writel(register_data, led_green.va_BSRR); // GPIOG2 引脚输出低电平,绿灯亮
	}
	else
	{
		register_data |= (0x01 << (2));
		writel(register_data, led_green.va_BSRR); // GPIOG2 引脚输出高电平,绿灯灭
	}

	/* 设置GPIOB5 输出电平*/
	if (write_data & 0x01)
	{
		register_data |= (0x01 << (5+16));
		writel(register_data, led_blue.va_BSRR); //GPIOB5 引脚输出低电平,灯亮
	}
	else
	{
		register_data |= (0x01 << (5));
		writel(register_data, led_blue.va_BSRR); //GPIOB5 引脚输出高电平,蓝灯灭
	}

	return 0;
}

/* 字符设备操作函数集*/
static struct file_operations led_chr_dev_fops =
{
	.owner = THIS_MODULE,
	.open = led_chr_dev_open,
	.write = led_chr_dev_write,
};

我们仅实现了两个字符设备操作函数,open 对应led_chr_dev_open 函数这是一个空函数。.write对应led_chr_dev_write 函数,这个函数接收应用程序传回的命令,根据命令控制RGB 三个灯的亮、灭。

  • 第14-19 行:使用copy_from_user 函数将用户空间的数据拷贝到内核空间。这里传递的数据是一个无符号整型数据。
  • 第21-61 行:解析命令,如何解析由我们自己决定。本例中使用数字的后三位从高到低依次对应红灯、绿灯、蓝灯,对应位是1 则亮灯否则熄灭。例如0x03 表示红灯灭,绿灯和蓝灯亮。0x07 表示全亮。具体实现过程很简单这里不再赘述。

编写测试应用程序

在驱动程序中我采用自动创建设备节点的方式创建了字符设备的设备节点文件,文件名可自定义,写测试应用程序时记得文件名即可。本例程设备节点名为“rgb_led”。测试程序很简单,源码如下所示。

列表7: 测试应用程序

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main(int argc, char *argv[])
{
	printf("led_tiny test\n");
	/* 判断输入的命令是否合法*/
	if(argc != 2)
	{
		printf(" command error ! \n");
		printf(" usage : sudo test_app num [num can be 0~7]\n");
		return -1;
	}


	 /* 打开文件*/
	int fd = open("/dev/rgb_led", O_RDWR);
	if(fd < 0)
	{
		printf("open file : %s failed !\n", argv[0]);
		return -1;
	}

	unsigned char command = atoi(argv[1]); //将受到的命令值转化为数字;


	/* 写入命令*/
	int error = write(fd,&command,sizeof(command));
	if(error < 0)
	{
		printf("write file error! \n");
		close(fd);
		/* 判断是否关闭成功*/
	}

	/* 关闭文件*/
	error = close(fd);
	if(error < 0)
	{
		printf("close file error! \n");
	}

	return 0;
}
  • 第10-15 行:简单判断输入是否合法,运行本测试应用程序时argc 应该为2。它由应用程序
    文件名和命令组成例如“./test_app < 命令值>”。
  • 第19-24 行:打开设备文件。
  • 第26-36 行:将终端输入的命令值转化为数字最终使用write 函数
  • 第39-43 行:关闭设备文件。

编译驱动程序

编译设备树

将rgb_led 节点添加到设备树中,并在内核源码目录执行如下命令。

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- stm32mp157_ebf_defconfig

make ARCH=arm -j4 CROSS_COMPILE=arm-linux-gnueabihf- dtbs

最终会在内核源码/arch/arm/boot/dts 下生成stm32mp157a-basic.dtb 这个设备树文件。

编译驱动和应用程序

执行make 命令,Makefile 和前面大致相同。最终会生成rgb_led.ko 和test_app 应用程序

程序运行结果

在板卡上的部分GPIO 可能会被系统占用,在使用前请根据需要改/boot/uEnv.txt 文件,可注释掉某些设备树插件的加载,重启系统,释放相应的GPIO 引脚。

如本节实验中,可能在鲁班猫系统中默认使能了LED 的设备功能,用在了LED 子系统。引脚被占用后,设备树可能无法再加载或驱动中无法再申请对应的资源。

方法参考如下:

在这里插入图片描述

取消LED 设备树插件,以释放系统对应LED 资源,操作如下:

取消LED 设备树插件,以释放系统对应LED 资源,操作如下:

如若运行代码时出现“Device or resource busy”或者运行代码卡死等等现象,请按上述情况检查并按上述步骤操作。

如出现Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root 用户权限,简单的解决方案是在执行语句前加入sudo 或以root 用户运行程序。

实验操作

将设备树、驱动程序和应用程序通过NFS 或SCP 等方式拷贝到开发板中。

替换原来的设备树/boot/dtbs/stm32mp157a-basic.dtb,并重启开发板。

执行如下命令加载驱动:

命令:

sudo insmod ./rgb-led.ko

在这里插入图片描述
驱动加载成功后直接运行应用程序如下所示。

命令:./test_app < 命令>

执行结果如下:

在这里插入图片描述

与此同时,你还会看到LED 呈现不同颜色的光。

命令是一个“unsigned char”型数据,只有后三位有效,每一位代表一个灯,从高到低依次代表红、绿、蓝,1 表示亮,0 表示灭。例如命令=4 则亮红灯,命令=7 则三个灯全亮。


参考资料:嵌入式Linux 驱动开发实战指南-基于STM32MP1 系列

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

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

相关文章

使用gitlab ci/cd来发布一个.net 项目

gitlab runner的安装和基本使用:https://bear-coding.blog.csdn.net/article/details/120591711安装并给项目配置完gitlab runner后再操作后面步骤。实现目标&#xff1a;master分支代码有变更的时候自动构建build。当开发人员在gitlab上给项目打一个tag标签分支的时候自动触发…

4.5.4 LinkedList

文章目录1.特点2.常用方法3.练习:LinkedList测试1.特点 链表,两端效率高,底层就是链表实现的 List接口的实现类&#xff0c;底层的数据结构为链表&#xff0c;内存空间是不连续的 元素有下标&#xff0c;有序允许存放重复的元素在数据量较大的情况下&#xff0c;查询慢&am…

代码随想录NO39 |0-1背包问题理论基础 416.分割等和子集

0-1背包问题理论基础 分割等和子集1. 0-1背包问题理论基础(二维数组实现)2. 0-1背包问题理论基础 二&#xff08;一维数组实现&#xff09;1. 0-1背包问题理论基础(二维数组实现) 背包问题一般分为这几种&#xff1a; 0-1背包问题&#xff1a;有n件物品和一个最多能背重量为w…

51单片机15单片机 时钟芯片DS1302【更新中】

前言 现在流行的串行时钟电路很多&#xff0c;如DS1302、 DS1307、PCF8485等。这些电路的接口简单、价格低廉、使用方便&#xff0c;被广泛地采用。 本文介绍的实时时钟电路DS1302是DALLAS公司的一种具有涓细电流充电能力的电路主要特点是采用串行数据传输&#xff0c;可为掉电…

配置与管理FTP服务器

FTP的概念及作用 FTP( 文件传输协议 ) 是目前Internet上流行的数据传输方法之一。利用FTP协议&#xff0c;可以在FTP服务器和客户机之间进行双向传输&#xff0c;既可以把数据从FTP服务器上下载到本地客户机&#xff0c;又可以从客户机上传数据到远程FTP服务器。FTP最初与WWW服…

[ECCV 2020] FGVC via progressive multi-granularity training of jigsaw patches

Contents IntroductionProgressive Multi-Granularity (PMG) training frameworkExperimentsReferencesIntroduction 不同于显式地寻找特征显著区域并抽取其特征,作者充分利用了 CNN 不同 stage 输出的特征图的语义粒度信息,并使用 Jigsaw Puzzle Generator 进行数据增强来帮…

MediaPipe之人体关键点检测>>>BlazePose论文精度

BlazePose: On-device Real-time Body Pose tracking BlazePose&#xff1a;设备上实时人体姿态跟踪 论文地址&#xff1a;[2006.10204] BlazePose: On-device Real-time Body Pose tracking (arxiv.org) 主要贡献: &#xff08;1&#xff09;提出一个新颖的身体姿态跟踪解决…

文件操作 -- IO

文章目录文件操作 -- IO文件 :文件路径 :文件的类型java 中的文件操作文件内容的相关操作字节流的读和写操作字符流的读和写操作代码案例代码案例一 &#xff1a;代码案例二 &#xff1a;代码案例三 &#xff1a;文件操作 – IO 文件 : 文件相比大家都不陌生把 &#xff0c; 打…

10 卷积神经网络CNN(基础篇)

文章目录全连接CNN过程卷积过程下采样过程全连接层卷积原理单通道卷积多通道卷积改进多通道总结以及课程代码卷积改进PaddingStride下采样过程大池化层&#xff08;Max Pooling&#xff09;简单卷积神经网络的实现课程代码本篇课程来源&#xff1a; 链接部分文本来源参考&#…

LSTM已死,Transformer当立(LSTM is dead. Long Live Transformers! ):上

回想一下在Seq2seq模型中,如何使用Attention。这里简要回顾一下【1】介绍的方法2(并以此为基础展开对Transformer的讨论)。 下图中包含一个encoder(左)和一个decoder(右)。对于decoder来说,给定一个输入,得到输出,如何进一步得到context vector 呢? 我们需要根据和…

网络工程师一定要学会的知识点:OSPF,今天给大家详细介绍

1. OSPF 概念OSPF&#xff08;Open Shortest Path First 开放式最短路径优先&#xff09;是一种动态路由协议&#xff0c;属于内部网关协议(Interior Gateway Protocol,简称 IGP)&#xff0c;是基于链路状态算法的路由协议。2. OSPF 的运行原理&#xff08;1&#xff09;OSPF 的…

后端开发必懂nginx面试40问

什么是Nginx&#xff1f; Nginx是一个 轻量级/高性能的反向代理Web服务器&#xff0c;用于 HTTP、HTTPS、SMTP、POP3 和 IMAP 协议。他实现非常高效的反向代理、负载平衡&#xff0c;他可以处理2-3万并发连接数&#xff0c;官方监测能支持5万并发&#xff0c;现在中国使用ngin…

Flink面试题

一 基础篇Flink的执行图有哪几种&#xff1f;分别有什么作用Flink中的执行图一般是可以分为四类&#xff0c;按照生成顺序分别为&#xff1a;StreamGraph-> JobGraph-> ExecutionGraph->物理执行图。1&#xff09;StreamGraph顾名思义&#xff0c;这里代表的是我们编写…

RabbitMQ安装及配置

目录1.下载和安装1.1 下载1.2. 安装1.3 测试1.4 卸载管理界面2.1 添加用户2.2 创建Virtual Hosts2.3. 设置权限1.下载和安装 1.1 下载 1.下载Erlang的rpm包 RabbitMQ是Erlang语言编写&#xff0c;所以Erang环境必须要有&#xff0c;注&#xff1a;Erlang环境一定要与RabbitMQ…

每天一道大厂SQL题【Day08】

每天一道大厂SQL题【Day08】 大家好&#xff0c;我是Maynor。相信大家和我一样&#xff0c;都有一个大厂梦&#xff0c;作为一名资深大数据选手&#xff0c;深知SQL重要性&#xff0c;接下来我准备用100天时间&#xff0c;基于大数据岗面试中的经典SQL题&#xff0c;以每日1题…

Learning C++ No.7

引言&#xff1a; 北京时间&#xff1a;20223/2/9/22:20&#xff0c;距离大一下学期开学还有2天&#xff0c;昨天收到好消息&#xff0c;开学不要考试了&#xff0c;我并不是害怕考试&#xff0c;考试在我心里&#xff0c;地位不高&#xff0c;可能只有当我挂了&#xff0c;才能…

自媒体人都在用的免费音效素材网站

视频剪辑、自媒体人必备的剪辑音效素材网站&#xff0c;免费下载&#xff0c;建议收藏&#xff01; 1、菜鸟图库 音效素材下载_mp3音效大全 - 菜鸟图库 菜鸟图库是一个综合性素材网站&#xff0c;站内涵盖设计、图片、办公、视频、音效等素材。其中音效素材就有上千首&#xf…

数学建模学习笔记(20)典型相关分析

典型相关分析概述&#xff1a;研究两组变量&#xff08;每组变量都可能有多个指标&#xff09;之间的相关关系的一种多元统计方法&#xff0c;能够揭示两组变量之间的内在联系。 典型相关分析的思想&#xff1a;把多个变量和多个变量之间的相关化为两个具有代表性的变量之间的…

【沁恒WCH CH32V307V-R1开发板读取板载温度实验】

【沁恒WCH CH32V307V-R1开发板读取板载温度实验】1. 前言2. 软件配置2.1 安装MounRiver Studio3. ADC项目测试3.1 打开ADC工程3.2 编译项目4. 下载验证4.1 接线4.2 演示效果5. 小结1. 前言 ADC 模块包含 2 个 12 位的逐次逼近型的模拟数字转换器&#xff0c;最高 14MHz 的输入时…

pandas——plot()方法可视化

pandas——plot()方法可视化 作者&#xff1a;AOAIYI 创作不易&#xff0c;如果觉得文章不错或能帮助到你学习&#xff0c;记得点赞收藏评论哦 在此&#xff0c;感谢你的阅读 文章目录pandas——plot()方法可视化一、实验目的二、实验原理三、实验环境四、实验内容五、实验步骤…