• 关键词:RK3399 Linux驱动 嵌入式

前言

对于无操作系统的SOC/MCU,当驱动一个引脚的时候,是直接设置寄存器的。同样,在linux系统下,也可以通过设置寄存器的方式来实现引脚的驱动。
本文梳理嵌入式Linux驱动开发中引脚驱动,实现LED控制,平台为RK3399

驱动开发流程

一般有SOC与外部设备连接的驱动开发遵循以下流程:

确认设备类型
确认硬件连接
编写驱动代码
编译内核模块文件
导入到系统并加载
编写测试程序

具体操作

应用程序对驱动的调用

在开始梳理驱动开发步骤之前先了解一下Linux操作系统对设备驱动的调用流程,有助于理解驱动程序。大多数情况下,应用程序运行再用户空间,驱动运行在内核空间,用户程序与硬件之间的交互,一般是通过系统的API再经过系统调用,进入内核空间,其过程如下所示:

应用程序调用系统的open close read write等API
通过系统调用进入内核
驱动程序中的open close read write等函数
具体硬件执行操作

确认设备类型

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函数
C库中的open函数
open系统调用
驱动的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

模块基地址寄存器偏移地址备注
PMUCRU0xFF750000PMUCRU_CLKGATE_CON10x0104使能GPIO0的时钟
PMUGRF0xFF320000PMUGRF_GPIO0B_IOMUX0x00004GPIO4C的功能复用
GPIOGPIO0:0xFF720000GPIO_SWPORTA_DDR0x0004数据方向
GPIO_SWPOERTA_DR0x0000数据寄存器

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驱动请查阅对应芯片的说明书
Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐