嵌入式Linux驱动开发之点亮LED(非设备树)
- 关键词:RK3399 Linux驱动 嵌入式
前言
对于无操作系统的SOC/MCU,当驱动一个引脚的时候,是直接设置寄存器的。同样,在linux系统下,也可以通过设置寄存器的方式来实现引脚的驱动。
本文梳理嵌入式Linux驱动开发中引脚驱动,实现LED控制,平台为RK3399
驱动开发流程
一般有SOC与外部设备连接的驱动开发遵循以下流程:
具体操作
应用程序对驱动的调用
在开始梳理驱动开发步骤之前先了解一下Linux操作系统对设备驱动的调用流程,有助于理解驱动程序。大多数情况下,应用程序运行再用户空间,驱动运行在内核空间,用户程序与硬件之间的交互,一般是通过系统的API再经过系统调用,进入内核空间,其过程如下所示:
确认设备类型
Linux驱动主要针对cup外部的设备。Linux将设备类型主要分为三大类:
- 字符设备
- 块设备
- 网络设备
字符设备是Linux系统中最基本的一类设备,是一个一个字节,按照字节流进行读写的操作设备,读写数据是分先后顺序的。常见的点亮LED,按键,I2C,SPL等都是字符设备,因此本文点亮LED即是字符设备驱动的开发。
确认硬件连接
本例中使用RK3399平台,LED连接到GPIO0B_5引脚上,由电路原理图可知,当引脚输出为高电平时,LED被点亮,引脚输出为低电平时,LED熄灭
编写驱动代码
简单的驱动代码-比如本例驱动引脚-可以在一个.c文件内实现。
代码内实现设备的具体操作函数:open函数、read函数、write函数、release函数并注册
代码内实现模块的加载函数、卸载函数,并对加载和卸载函数进行注册;
设备具体操作函数
驱动模块注册到内核后供应用程序使用,如前所述,应用程序在使用驱动程序时,总会使用open()、read()、write()、close()等函数,但是应用程序运行在用户空间,而Linux驱动属于内核的一部分,运行于内核空间,用户空间的函数,不能直接对内核进行操作,当在用户空间的函数要实现操作内核时,必须使用称为“系统调用”的方法,举例,比如在应用空间使用open()函数,具体的流程如下
应用程序使用的函数在具体驱动程序中也应有与之对应的函数,对于字符型设备,open()函数、read()函数、write()函数、release()函数是需要的,因此具体驱动程序中也需要实现对应的函数,并完成映射
设备具体操作函数是驱动程序中一个重要组成部分
具体操作函数与内核中的对应函数通过file_operations结构体完成映射
内核模块
Linux驱动有两种运行方式:一种是将驱动编译进Linux内核中,当Linux内核启动时,就会自动运行驱动程序;另一种是将驱动编译成模块,即内核模块(Linux下模块文件的扩展名为.ko),在Linux内核启动后,使用insmod命令加载内核模块,运行驱动程序。
本文这里采用内核模块的方式,编写驱动GPIO的内核模块程序,编译为.ko文件后加载到Linux系统运行
内核模块于内核,就像插件于浏览器,内核模块使功能模块化,具有以下特点:
- 内核模块本身不被编译进内核
- 内核模块一但被加载,它就和内核中其他部分完全一样了
内核模块需要加载到内核才能使用,加载内核模块可以用命令:insmode
相应的,内核模块如果不需要了,也可以卸载掉,可以用命令:rmmod
驱动代码的一个重要内容就是内核模块程序
一个简单的内核模块程序如下:
/*
*a simple kernel module:hello
*
*Copyright (C) 2024 Gump
*
*Licensed under GPLv2 or later
*/
#include <linux/init.h>
#include <linux/module.h>
static init __init hello_init(void)
{
printk(KERN_INFO "Hello Gump!\n");
return 0;
}
module_init(hello_init);
static void __exit hello_exit(void)
{
printk(KERN_INFO "Bye Gump!\n");
}
module_exit(hello_exit);
MODULE_AUTHOR("Gump");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("A simple Hello Module");
MODULE_ALIAS("a simple Hello");
一个内核模块程序主要有如下几部分:
- 模块加载函数
内核模块加载函数一般以_ _init标识声明,如
static int __init initialization_function(void)
{
/*初始化代码*/
}
module_init(initionlization_function)
模块加载函数以module_init(函数名)的形式被指定
当通过insmod或modprobe命令加载内核模块时,模块加载函数会自动被内核执行,一般在模块加载函数内完成引脚寄存器配置,地址映射和设备注册等操作
- 模块卸载函数
当通过rmmod命令卸载某模块时,卸载函数会自动被内核执行
内核模块卸载函数一般以__exit标识声明,如
static void __exit cleanup_function(void)
{
/*释放代码*/
}
module_exit(cleanup_function)
模块卸载函数以moduel_exit(函数名)的形式被指定
当卸载模块时,模块卸载函数会被自动执行,一般在卸载函数内完成取消地址映射和设备注销的功能操作
- 模块许可证声明
许可证(LICENSE)声明描述内核模块的许可权限,如果不声明LICENSE,模块被加载时,将收到内核被污染(Kernel Tainted)的警告
大多数情况下,内核模块应遵循GPL兼容许可,Linux内核模块最常见的是以MODULE_LICENSE("GPL v2")语句声明模块采用GPL v2许可 - 模块参数(可选)
- 模块导出符号(可选)
内核模块之间也存在依赖关系,内核模块通过EXPORT_SYMBOL宏导出在模块内部定义的函数或变量提供给其他模块使用,称为符号。如A模块需要用到B模块的函数才能正常工作,称为A模块依赖B模块。 - 模块作者等信息声明(可选)
寄存器及其地址
如前所述,Linux系统下,驱动GPIO也要配置引脚的寄存器,因此配置寄存器是驱动代码内不可缺少的内容。根据RK3399说明书对GPIO的说明,如果要控制一个引脚,需要设置其
时钟使能寄存器: PMUCRU_CLKGATE_CON1
多功能复用寄存器:PMUGRF_GPIO0B_IOMUX
方向寄存器:GPIO_SWPORTA_DDR
数据寄存器:GPIO_SWPORTA_DR
| 模块 | 基地址 | 寄存器 | 偏移地址 | 备注 |
|---|---|---|---|---|
| PMUCRU | 0xFF750000 | PMUCRU_CLKGATE_CON1 | 0x0104 | 使能GPIO0的时钟 |
| PMUGRF | 0xFF320000 | PMUGRF_GPIO0B_IOMUX | 0x00004 | GPIO4C的功能复用 |
| GPIO | GPIO0:0xFF720000 | GPIO_SWPORTA_DDR | 0x0004 | 数据方向 |
| GPIO_SWPOERTA_DR | 0x0000 | 数据寄存器 |
PMUCRU_CLKGATE_CON1 的第4位设置为0使能时钟,第(4+16)位置为1使能该位可写
PMUGRF_GPIO0B_IOMUX 的第12位和11位置为0使GPIO0B_5引脚工作在gpio模式
GPIO_SWPORTA_DDR 的第13位配置为1,配置为输出模式
GPIO_SWPORTA_DR 是数据寄存器,当引脚配置为输出模式时,向该寄存器写数值,当配置为输入模式时,读取该寄存器的值,本例这里向该寄存器的第13位写1表现为GPIO0B_5引脚输出高电平
地址映射
地址映射的目的是将物理地址(本文这里的物理地址指寄存器地址)映射到虚拟地址,CPU通过访问虚拟地址达到访问物理地址的目的。
将物理地址映射到虚拟地址的好处是实现内存保护
涉及物理地址和虚拟地址映射的函数是ioremap()和iounmap()
- ioremap()函数
该函数用户获取指定的物理地址对应的虚拟地址,该函数有两个参数,一个返回值,如下
GPIO0_DR = ioremap(GPIO_SWPORTA_DR,4)- 其中GPIO_SWPORTA_DR是要映射的物理地址器是位置
- 4是要映射的空间大小,本文RK3399的寄存器是32位的,因此映射长度为4
- GPIO0_DR是映射后的虚拟地址
映射后直接对GPIO0_DR进行读写就相当于对寄存器GPIO_SWPORTA_DR操作
- iounmap()函数
该函数用于释放ioremap()函数所做的映射,该函数有一个参数,如下
`iounmap(GPIO_DR);
完成物理地址到虚拟地址的映射后,就可以直接通过指针访问这些地址,但是Linux内核不建议这么做,而是推荐使用一组操作函数来实现对映射后的地址进行读写操作:
- 读操作函数:readl(),如
readl(GPIO0_CRU);
- 写操作函数:writel(),如
writel(val GPIO0_DR);- 其中val是写入的数值,这里是u32类型
设备的注册与注销
对字符设备而言,当驱动模块加载成功后,需要注册字符设备。同样,当卸载模块前,也应先注销设备
注册设备需要明确设备的设备号
设备号
为了方便管理,Linux中每个设备都有一个设备号,在注册设备时需要知道设备号
设备号由主设备号和次设备号组成,主设备号表示一个具体的驱动,次设备号表示使用这个驱动的各设备
Linux提供了一个名为dev_t的数据类型表示设备号,dev_t定义在Linux内核源代码的/include/linux/types.h中
dev_t是一个无符号32位数据类型,高位12位为主设备号,低位20位为次设备号
设备号分配
-
静态分配
在编写驱动程序时给设备指定一个设备号,但这样容易带来设备号冲突问题,可以进入到运行在嵌入式SOC上的linux系统内/proc/devices文件查看哪些设备号没有被占用
静态分配设备号,要先注册设备号,再将设备注册给系统 -
动态分配
Linux社区推荐使用动态分配设备号的方式。在注册设备号前,先申请一个设备号,系统会自动给一个没有使用的设备号,这样避免冲突。申请设备号的函数如下:
int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name)- dev-保存申请到的设备号
- baseminor-次设备号起始地址
- count-要申请的设备号数量
- name-设备名字
动态分配设备号,无需再单独注册设备号,可直接给系统注册设备
设备号释放
注销设备后要释放掉设备号,设备号释放函数如下:
void unregister_chrdev_region(dev_t from,unsigned count)
- from-要释放的设备号
- count-表示从from开始,要释放的设备号数量
字符设备结构
在Linux中,使用cdev结构表示一个字符设备,cdev结构体定义在/include/linux/cdev.h中:
struct cdev{
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head List;
dve_t dev;
unsigned init count;
}
其中
- ops 就是指具体操作函数与内核的对应函数的映射函数
file_operations - dev 就是设备号
在驱动程序中要定义一个cdev结构体变量,这个变量就代表一个字符设备
在驱动程序中,给字符设备的code变量定义好各变量后,使用cdev_init()函数对其进行初始化,原型如下:
void cdev_init(struct cdev *cdev,const struct file_operations *fops)
注册函数
设备初始化后要用cdev_add()函数向Linux注册该设备,原型如下:
int cdev_add(struct cdev *p,dev_t dev,unsigned count)
相应的,卸载驱动时,也应注销字符设备,使用cdev_del函数,原型如下:
void cdev_del(struct cdev *p)
注意,如果设备号是静态分配的,还要先将设备号进行注册,注册函数如下:
int register_chardev_region(dev_t from,unsigned count,const char *name)
其中
- from 是要注册的起始设备号,也就是给定的设备号
- count 是要注册的数量,一般都是1个
- name 是设备名
静态分配设备号要先注册设备号,再用cdev_add向系统注册设备
创建设备节点文件
如前所述,内核模块加载到内核才能运行,加载到内核后,还需在/dev路径下创建设备节点文件,应用程序正是通过操作这个节点文件来完成对具体设备的操作。
节点文件可以在加载内核模块后使用mkond命令手动创建,也可以在驱动程序中实现自动创建。
在嵌入式Linux系统内可以用mdev来实现设备节点文件的自动创建与删除
自动创建设备节点的工作是在驱动程序的入口函数(加载函数)中完成的,一般在cdev_add()函数后面添加自动创建设备节点文件的相关代码。
具体操作:
- 使用class_create()函数创建一个class类,原型如下
struct class *class_create(struct module *owner,const *name)
其中:
owner一般为THIS_MODULE
name是类名
对应的,在卸载驱动时,需要删掉类,可以用函数class_destory()函数,原型:
void calss_detory(struct calss *cls) - 创建节点文件
创建好类后,还不能算是实现了自动创建节点文件,还需要在这个类下创建一个设备,使用device_create()函数来实现,函数原型:
struct device *device_create(struct class *class,
struct device *parent,
dve_t devt,
void *drvdata,
const char *fmt,...)
其中
- calss 是设备要建在哪个类下面
- parent 是父设备,一般为NULL
- devt 是设备的设备号
- drvdat 是设备可能会使用的数据,一般为NULL
- fmt 是设备的名字,如果fmt=gumpLED,则会生成/dev/gumpLED这个设备文件
对应的,在卸载驱动时,需要删除创建的设备节点文件,函数为device_destory()
设置文件私有属性
每个硬件设备都有一些属性,比如主设备号,类,设备开关状态,在写驱动程序时,可以将这些属性和设备用一个结构体表示。如下:
struct sample_dev{
dve_t devid ; //设备号
struct cdev cdev; //字符设备
struct class *class; //类
struct device *device; //设备
int major; //主设备号
int minor; //次设备号
}
struct sample_dev sample_chardev;
在编写具体操作函数open()函数时,可以将上面的结构体作为私有数据添加到设备文件中,例如:
static int sample_open(struct inode *inode,struct file *filp)
{
filp -> private_data = &sample_chardev;
return 0;
}
编译为内核模块
在运行Linux的嵌入式芯片(本文是RK3399)上使用驱动程序,需要在PC平台交叉编译驱动程序成为.ko文件,再加载到内核运行。交叉编译相关知识及交叉编译工具链请查询其他资料,本文这里直接说明Makefile文件的内容,如下:
KERNELDIR:=/home/gump/rk3399/kernel-develop-4.4
CURRENT_PATH:=$(shell pwd)
obj-m:=ledDriv.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
其中
- KERNELDIR表示Linux内核源码目录,使用绝对路径,根据实际路径填写
- CURRENT_PATH表示驱动程序所在目录,这里用pwd命令直接获取
- obj-m表示直接将ledDriv.c文件直接编译为.ko文件
- 其他具体编译命令暂时不做解释,按照编写就行
编译内核模块时,Linux源代码版本应和嵌入式在运行的Linux版本相同
编译命令:(可根据自己的交叉编译工具更改)
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
导入到系统
利用fileZilla或MobaXterm等软件将编译好的.ko文件导入到嵌入式Linux文件系统内
加载模块
-
使用
modprobe命令加载ko文件,modprobe命令会分析模块的依赖,然后将所有依赖模块都加载到内核中,modprobe命令要求ko文件应放置在根文件系统的/lib/modules/x.xx.xxx路径下,x.xx.xxx是Linux内核版本号(笔者尝试使用这种方法,但总是报找不到文件问题) -
使用
insmod命令,本例的驱动模块没有依赖,可用此命令,这里强制加载方式:
insmod -f xxx.ko -
在Linux中一切皆文件,驱动模块加载成功后会在
/dev目录下生成一个相应的文件,应用程序通过对这个名为/dev/xxx的文件进行相应的操作,即可实现对硬件的操作。
编写测试程序
测试程序也是C文件
测试程序调用设备节点文件控制设备,这里编写测试代码实现引脚的高低电平变化,实现LED的亮灭控制
测试程序编译注意编译工具链,笔者这里的编译命令:
aarch64-linux-gnu-gcc ledTest.c -o ledTest
如果嵌入式Linux内安装有编译器,可以将测试程序直接在嵌入式平台编译
实例
- 平台为RK3399
- 控制引脚GPIO0B_5
- 使用自动获取设备号的形式
驱动程序如下(文件命名为ledDriv.c):
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
/*************************************
*FileName:ledDriv.c
*Fuc: RK3399,GPIO0B_5 driver
*
*Author: PineLiu
************************************/
#define CHAR_CNT 1 //设备数量
#define LED_NAME "gumpled" //设备名称
#define LEDON 1 //点亮
#define LEDOFF 0 //熄灭
//寄存器物理地址
#define PMUCRU_CLKGATE_CON1 0XFF750104
#define PMUGRF_GPIO0B_IOMUX 0XFF320004
#define GPIO_SWPORTA_DDR 0XFF720004
#define GPIO_SWPORTA_DR 0XFF720000
//重映射后的寄存器虚拟地址
static void __iomem *GPIO0_DR; //数据寄存器
static void __iomem *GPIO0_DDR; //方向寄存器
static void __iomem *GPIO0_IOMUX; //复用寄存器
static void __iomem *GPIO0_CRU; //时钟寄存器
//声明设备结构体(该设备包含的属性)
struct newexternal_dev{
dev_t devid; //设备号
struct cdev cdev; //字符设备
struct class *class; //设备节点类 主动创建设备节点文件用
struct device *device; //设备节点 主动创建设备节点文件用
int major; //主设备号
int minor; //次设备号
};
//声明一个外部设备,本文这里是字符设备
struct newexternal_dev charled;
//LED打开/关闭
void led_switch(u8 sta)
{
u32 val = 0;
if(sta == LEDON){
val = readl(GPIO0_DR);
val |= (1<<13);
writel(val,GPIO0_DR);
}else if(sta == LEDOFF){
val = readl(GPIO0_DR);
val &= ~(1<<13);
writel(val, GPIO0_DR);
}
}
//===========================================================================
//以下实现设备的具体操作函数:open函数、read函数、write函数和release函数
//===========================================================================
//打开设备
static int led_open(struct inode *inode, struct file *filp)
{
filp -> private_data = &charled; //设置私有数据
return 0;
}
//从设备读取数据
static ssize_t led_read(struct file *filp,char __user *buf,
size_t cnt,loff_t *offt)
{
return 0;
}
//向设备写数据
//filp:设备文件,表示打开的文件描述
//buf :保存着要向设备写入的数据
//cnt :要写入的数据长度
//offt:相对于文件首地址的偏移
//
static ssize_t led_write(struct file *filp,const char __user *buf,
size_t cnt,loff_t *offt)
{
int retvalue;
unsigned char databuf[1];
unsigned char ledstat;
retvalue = copy_from_user(databuf,buf,cnt);
if(retvalue < 0){
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstat = databuf[0]; //获取状态值
if(ledstat == LEDON){
led_switch(LEDON);
}else if(ledstat == LEDOFF){
led_switch(LEDOFF);
}
return 0;
}
//关闭,释放设备
//filp: 要关闭的设备文件描述
//
static int led_release(struct inode *inode,struct file *filp)
{
return 0;
}
//==========================================================================
//以下实现的设备具体函数与内核的对应函数的映射
//==========================================================================
//映射设备操作函数
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
//==========================================================================
//内核模块相关
//==========================================================================
//内核模块加载函数或称为驱动入口函数
static int __init led_init(void)
{
u32 val = 0;
//=========初始化LED
//寄存器地址映射
GPIO0_DR = ioremap(GPIO_SWPORTA_DR, 4);
GPIO0_DDR = ioremap(GPIO_SWPORTA_DDR, 4);
GPIO0_IOMUX = ioremap(PMUGRF_GPIO0B_IOMUX, 4);
GPIO0_CRU = ioremap(PMUCRU_CLKGATE_CON1, 4);
//使能时钟
// GPIO0_CRU |= (1 << (3 + 16));
// GPIO0_CRU &= ~(1 << 3);
val = readl(GPIO0_CRU);
val |= (1 << (3+16));
val &= ~(1 << 3);
writel(val,GPIO0_CRU);
//设置GPIO0B_5引脚复用功能为GPIO
// GPIO0_IOMUX |= (3 << (10+16));
// GPIO0_IOMUX &= ~(3 << 10);
val = readl(GPIO0_IOMUX);
val |= (3 << (10+16));
val &= ~(3 << 10);
writel(val,GPIO0_IOMUX);
//配置GPIO0B_5引脚为输出模式
// GPIO0_DDR |= (1 << 13);
val = readl(GPIO0_DDR);
val |= (1 << 13);
writel(val,GPIO0_DDR);
//配置GPIO0B_5引脚默认关闭LED
// GPIO0_DR &= ~(1 << 13);
val = readl(GPIO0_DR);
val &= ~(1 << 13);
writel(val,GPIO0_DR);
//===========注册设备
//创建设备号
if(charled.major){ //定义了设备号
charled.devid = MKDEV(charled.major,0);
register_chrdev_region(charled.devid,CHAR_CNT,LED_NAME);
}else{ //没有定义设备号,要向系统申请设备号
alloc_chrdev_region(&(charled.devid),0,CHAR_CNT,LED_NAME);
charled.major = MAJOR(charled.devid);
charled.minor = MINOR(charled.devid);
}
printk("charled major = %d,minor = %d\r\n",charled.major,charled.minor);
//初始化cdev
charled.cdev.owner = THIS_MODULE;
cdev_init(&charled.cdev,&led_fops);
//向系统注册设备
cdev_add(&charled.cdev,charled.devid,CHAR_CNT);
//创建类
charled.class = class_create(THIS_MODULE,LED_NAME);
if(IS_ERR(charled.class)){
return PTR_ERR(charled.class);
}
//创建设备节点
charled.device = device_create(charled.class,NULL,charled.devid,NULL,LED_NAME);
if(IS_ERR(charled.device)){
return PTR_ERR(charled.device);
}
return 0;
}
//内核模块卸载函数
static void __exit led_exit(void)
{
//取消映射
iounmap(GPIO0_CRU);
iounmap(GPIO0_IOMUX);
iounmap(GPIO0_DDR);
iounmap(GPIO0_DR);
//注销设备
cdev_del(&charled.cdev);
unregister_chrdev_region(charled.devid,CHAR_CNT);
device_destroy(charled.class,charled.devid);
class_destroy(charled.class);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Gump");
这里不再对代码展开说明,驱动程序代码内容与上面描述相同
测试程序:
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/*************************************************
*FileName:ledTest.c
*Func: GPIO driver module test
*
*Author: pineliu
* **********************************************/
#define LEDOFF 0
#define LEDON 1
int main(int argc,char *argv[])
{
int fd, retvalue;
char *filename;
unsigned char databuf[1];
if(argc != 3){
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
fd = open(filename,O_RDWR);
if (fd < 0){
printf("file %s open failed!\r\n",argv[1]);
return -1;
}
databuf[0] = atol(argv[2]);
retvalue = write(fd,databuf,sizeof(databuf));
if(retvalue < 0){
printf("led Control Error!\r\n");
close(fd);
return -1;
}
retvalue = close(fd);
if(retvalue < 0){
printf("file %s close failed!\r\n",argv[1]);
return -1;
}
return 0;
}
这里不对测试程序展开说明
将测试程序直接放到嵌入式Linux系统内用自带的编译器编译后生成可执行文件,或在PC平台编译为嵌入式平台可执行文件
本文使用在PC平台编译:
aarch64-linux-gnu-gcc ledTest.c -o ledTest
将获得的ledTest文件放到嵌入式Linux系统内,并赋予可执行权限
chmod +x ledTest
使用如下命令点亮led:
./ledTest /dev/gumpled 1
使用如下命令关闭led:
./ledTest /dev/gumpled 0
实验证实以上驱动代码和测试代码可行
总结
- 驱动程序由C语言编写,将之编译为内核模块.ko文件
- 将ko文件安装到内核空间,在/dev路径下生成设备文件
- 驱动程序内实现系统调用的open()、read()、write()、close()等函数
- 驱动程序内实现寄存器控制
- 驱动程序内实现设备号注册、注销
- 驱动程序内实现模块加载、卸载
- 在驱动程序内,使用结构体表示一个设备,设备属性内包含设备类型,设备号等信息
- 涉及到具体芯片的GPIO驱动请查阅对应芯片的说明书
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)