RK3568笔记三十五:LED驱动开发测试

news2024/11/15 11:12:07

若该文为原创文章,转载请注明原文出处。

字符设备驱动程序的基本框架,主要是如何申请及释放设备号、添加以及注销设备,初始化、添加与删除 cdev 结构体,并通过 cdev_init 函数建立 cdev 和 file_operations 之间的关联,cdev 结构体和 file_operations 结构体。

记录编写第一个真正的 Linux 字符设备驱动,点灯。

使用的开发板是正点原子的ATK-DLRK3568.

一、查看原理图

通过查看原理图,得知LED控制引脚接到了 GPIO0_C0上,通过三极管控制LED,GPIO0_C0输出高电平,LED亮,输出低电平,LED灭。

二、GPIO介绍

GPIO(General Purpose Input/Output Port):通用输入输出端口。

除作为一般的输入/输出功能外,还可以配置为中断和模拟UART、CAN、PWM、I2C、SDMMC、CLK等功能。

比如 GPIO0_C0 这个 IO 就可以用 作:GPIO,PWM1_M0,GPU_AVS 和 UART0_RX 这四个功能,所以我们首先要设置好当前引 脚用作什么功能。

rk356x 系列对应的文档为:

• Rockchip_RK3568_TRM_Part1_xxx.pdf

• Rockchip_RK3566_Datasheet_xxx.pdf

• Rockchip_RK3568_Datasheet_xxx.pdf

1、GPIO分组

RK3568共160个GPIO引脚,复用型引脚分为 5 组 (GPIO0~4),每组里面都有 32 个复 用型引脚,而且又分为 4 个小组 (A、B、C、D),每个小组 8 个引脚 (0~7)。例如:GPIO0_C7 是 GPIO0 大组,第 3 个小组,第 8 个引脚。

要查找GPIO对应的配置寄存器地址,必须知道他属于哪个分组。

对于 LED 灯的控制进行控制,也就是对上述 GPIO 的寄存器进行读写操作。
可大致分为以下几个 步骤:
• 使能 GPIO 时钟 (默认开启,不用设置)
• 设置引脚复用为 GPIO(复位默认为 GPIO,不用配置)
• 设置引脚属性 (上下拉、速率、驱动能力, 默认)
• 控制 GPIO 引脚为输出,并输出高低电平
因为 GPIO 的时钟默认开启,引脚默认复用为 GPIO,我们只需要配置 GPIO 的引脚输入输出模式 及电平即可。

2、GPIO引脚号计算方法

pins = 32*bank_num + 8*group + x
bank_num : 0 ~ 4,对应GPIO 0~4
group    : 0 ~ 3,对应GPIO A~D
GPIO0_C0:
GPIO0_C2 = 32*0(bank_num) + 8*2(group) + 0 = 16

根据计算GPIO0_C0序号为16。在后面驱动代码时会用到。

2、寄存器配置

这里以GPIO0_C0为例,查看Rockchip_RK3568_TRM_Part1手册可知,GPIO0 组复用功能是在 PMU_GRF 寄存器,实验中需要对 GPIO 进行配置,一般情况下需要对 GPIO 的复用寄存器,方向寄存器,数据寄存器进行配置, 和复用相关的总共 8 个寄存器。

1. 查找复用寄存器

搜索 GPIO0_C0,GPIO0_C0_sel 在 PMU_GRF_GPIO0C_IOMUX_H 上,偏移地址为 0x0010。GPIO0_C0可以通过控制[2:0]位来选择复用为哪个功能,我们要控制led 灯,所以功能要复用为 GPIO。

复用寄存器的基地址:

GPIO0_C0 设置为 GPIO,所以 PMU_GRF_GPIO0C_IOMUX_L 的 bit2:0 这三位
设置 000。另外 bit18:16 要设置为 111,允许写 bit2:0。

2. 查找方向寄存器

通过设置 GPIO 寄存器设置输入输出、高低电平、中断、抖动等一些引脚的驱动能力,电
气属性等,主要通过设置 General Register Files (GRF)(以 GPIO0 组为例,详细自行参考
Rockchip_RK35xx_TRM_Part1 手册):

• GPIO_SWPORT_DDR_L:低位引脚数据方向寄存器,控制输入或者输出。
• GPIO_SWPORT_DDR_H:高位引脚数据方向寄存器,控制输入或者输出。

通过寄存器描述,该寄存器有高 16bit 和低 16bit,高 16bit 控制低 16bit 的 写使能,低 16bit 控制 GPIO 的输出方向, 0:输入,1:输出。

GPIO0_C0属于 GPIO0 中 A-D 组总计 64 个引脚中的高 32 引脚范围,所以需要将 GPIO_SWPORT_DDR_H 寄存器的第 0bit 位和 16bit 位置 1,允许写 bit16。

GPIO0~GPIO4 的基地址:

GPIO_SWPORT_DDR_H 寄存器地址计算 

Operational Base + offset = 0xFDD60000 + 0x000C = 0xFDD6000C

3. GPIO 引脚高低电平设置

• GPIO_SWPORT_DR_L:低位引脚数据寄存器,设置高低电平。
• GPIO_SWPORT_DR_H:高位引脚数据寄存器,设置高低电平。

GPIO_SWPORT_DR_L 和 GPIO_SWPORT_DR_H 寄存器有高 16bit 和低 16bit,高 16bit 控制低 16bit 的写 使能,低 16bit 控制 GPIO 的高低电平。

GPIO0_C0属于 GPIO0 中 A-D 组总计 64 个引脚 中高的 32 引脚范围,所以需要将 GPIO_SWPORT_DR_H 寄存器的第0bit 位和 16bit 位置 1。

4. 总结

复用关系寄存器的基地址为 0xFDC20000 ,偏移地址为 0x0010
所以要操作的地址为基地址+偏移地址=0xFDC20010 

GPIO 的基地址为 0xFDD60000,偏移地址为 0x000C,
所以方向寄存器要操作的地址为基地址+偏移地址=0xFDD6000C 

GPIO 的基地址为 0xFDD60000,偏移地址为 0x0004,
所以数据寄存器要操作的地址为基地址+偏移地址=0xFDD60004

 

三、驱动程序编写

1、编写led_cdev.c驱动文件

#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>

#define DEV_NAME            "led_chrdev"
#define DEV_CNT                 (1)

#define GPIO0_BASE (0xfdd60000)

//每组GPIO,有2个寄存器,对应32个引脚,每个寄存器负责16个引脚;
//一个寄存器32位,其中高16位都是使能位,低16位对应16个引脚,每个引脚占用1比特位
#define GPIO0_DR_L (GPIO0_BASE + 0x0000)
#define GPIO0_DR_H (GPIO0_BASE + 0x0004)

#define GPIO0_DDR_L (GPIO0_BASE + 0x0008)
#define GPIO0_DDR_H (GPIO0_BASE + 0x000C)

static dev_t devno;
struct class *led_chrdev_class;

struct led_chrdev {
	struct cdev dev;
	unsigned int __iomem *va_dr; 	// 数据寄存器,设置输出的电压
	unsigned int __iomem *va_ddr; 	// 数据方向寄存器,设置输入或者输出

	unsigned int led_pin; // 偏移
};

/* 打开设备函数 */
static int led_chrdev_open(struct inode *inode, struct file *filp)
{	
	unsigned int val = 0;
	struct led_chrdev *led_cdev = (struct led_chrdev *)container_of(inode->i_cdev, struct led_chrdev,dev);
	filp->private_data = container_of(inode->i_cdev, struct led_chrdev, dev);

	printk("open\n");
	//设置输出模式
	val = ioread32(led_cdev->va_ddr);
	val |= ((unsigned int)0x1 << (led_cdev->led_pin+16));
	val |= ((unsigned int)0X1 << (led_cdev->led_pin));
	iowrite32(val,led_cdev->va_ddr);
	
	//输出高电平
	val = ioread32(led_cdev->va_dr);
	val |= ((unsigned int)0x1 << (led_cdev->led_pin+16));
	val |= ((unsigned int)0x1 << (led_cdev->led_pin));
	iowrite32(val, led_cdev->va_dr);

	return 0;
}

static int led_chrdev_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* 从设备读取数据 */
static ssize_t led_chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{	
	printk("This is led_chrdev_read\r\n");
	
	return 0;
}

/* 向设备写入数据函数 */
static ssize_t led_chrdev_write(struct file *filp, const char __user * buf,
				size_t count, loff_t * ppos)
{
	unsigned long val = 0;
	char ret = 0;

	struct led_chrdev *led_cdev = (struct led_chrdev *)filp->private_data;
	printk("write \n");
	get_user(ret, buf);
	val = ioread32(led_cdev->va_dr);
	printk("val = %lx\n", val);
	if (ret == '0' || ret == 0){
		val |= ((unsigned int)0x1 << (led_cdev->led_pin+16));
		val &= ~((unsigned int)0x01 << (led_cdev->led_pin));   /*设置GPIO引脚输出低电平*/
	}
	else{
		val |= ((unsigned int)0x1 << (led_cdev->led_pin+16));
		val |= ((unsigned int)0x01 << (led_cdev->led_pin));    /*设置GPIO引脚输出高电平*/
	}
	iowrite32(val, led_cdev->va_dr);
	printk("val = %lx\n", val);
	return count;
}

/* 设备操作函数 */
static struct file_operations led_chrdev_fops = {
	.owner = THIS_MODULE,          // 将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
	.open = led_chrdev_open,       // 将 open 字段指向 chrdev_open(...)函数
	.read = led_chrdev_read,       // 将 open 字段指向 chrdev_read(...)函数
	.write = led_chrdev_write,     // 将 open 字段指向 chrdev_write(...)函数
    .release = led_chrdev_release, // 将 open 字段指向 chrdev_release(...)函数
};

static struct led_chrdev led_cdev[DEV_CNT] = {
	{.led_pin = 0}, 	//偏移,高16引脚,GPIO0_C0
};

/* 驱动入口函数 */
static __init int led_chrdev_init(void)
{
	int i = 0;
	dev_t cur_dev;

	printk("led_chrdev init (lubancat2  GPIO0_C7)\n");
	
	/*0 将物理地址转化为虚拟地址 */
	led_cdev[0].va_dr   = ioremap(GPIO0_DR_H, 4);	 //
	led_cdev[0].va_ddr  = ioremap(GPIO0_DDR_H, 4);	 // 

    /*1 创建设备号 */
	alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME);
	
    /*2 创建类 */
	led_chrdev_class = class_create(THIS_MODULE, "led_chrdev");

	for (; i < DEV_CNT; i++) {
		/*3 初始化 cdev */
		cdev_init(&led_cdev[i].dev, &led_chrdev_fops);
		led_cdev[i].dev.owner = THIS_MODULE;
        
		/*4 获取主设备号和次设备号 */
		cur_dev = MKDEV(MAJOR(devno), MINOR(devno) + i);
 
        /*5 添加一个 cdev,完成字符设备注册到内核 */
		cdev_add(&led_cdev[i].dev, cur_dev, 1);

        /*6 创建设备*/
		device_create(led_chrdev_class, NULL, cur_dev, NULL, DEV_NAME "%d", i);
	}

	return 0;
}


/* 驱动出口函数 */
static __exit void led_chrdev_exit(void)
{
	int i;
	dev_t cur_dev;
	printk("led chrdev exit (lubancat2  GPIO0_C7)\n");
	
	/*注销字符设备*/
	for (i = 0; i < DEV_CNT; i++) {
		iounmap(led_cdev[i].va_dr); 		// 释放模式寄存器虚拟地址
		iounmap(led_cdev[i].va_ddr); 	// 释放输出类型寄存器虚拟地址
	}

	for (i = 0; i < DEV_CNT; i++) {
		
		cur_dev = MKDEV(MAJOR(devno), MINOR(devno) + i);

        //删除设备
		device_destroy(led_chrdev_class, cur_dev);

        // 删除 cdev
		cdev_del(&led_cdev[i].dev);

	}
	

	// 注销设备号
	unregister_chrdev_region(devno, DEV_CNT);
	
	// 删除类
	class_destroy(led_chrdev_class);

}

module_init(led_chrdev_init);
module_exit(led_chrdev_exit);

MODULE_AUTHOR("yifeng");
MODULE_LICENSE("GPL");

总结:

模块加载

1、初始化 LED 灯结构体成员,将物理寄存器的地址映射到虚拟地址空间

2、向动态申请一个设备号

3、创建设备类

4、绑定 led_cdev led_chrdev_fops

5、注册设备

6、创建设备

模块卸载

1、删除设备

2、注销设备

3、释放被占用的设备号

2、编写makefile

KERNELDIR := /home/alientek/rk3568_linux_sdk/kernel
ARCH=arm64
CROSS_COMPILE=/opt/atk-dlrk356x-toolchain/usr/bin/aarch64-buildroot-linux-gnu-

export  ARCH  CROSS_COMPILE

CURRENT_PATH := $(shell pwd)
obj-m := led_cdev.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

编译

3、编写APP应用

ledApp.c

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"


#define LEDOFF 	0
#define LEDON 	1

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
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];

	/* 打开led驱动 */
	fd = open(filename, O_RDWR);
	if(fd < 0){
		printf("file %s open failed!\r\n", argv[1]);
		return -1;
	}

	databuf[0] = atoi(argv[2]);	/* 要执行的操作:打开或关闭 */

	/* 向/dev/led文件写入数据 */
	retvalue = write(fd, databuf, sizeof(databuf));
	if(retvalue < 0){
		printf("LED Control Failed!\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;
}

编译

/opt/atk-dlrk356x-toolchain/bin/aarch64-buildroot-linux-gnu-gcc ledApp.c -o ledApp

4、测试

测试有两个,一是直接测试,二是使用APP应用程序测试。

在测试前需要关闭心跳灯

echo none > /sys/class/leds/work/trigger

加载LED驱动:

insmod led_cdev.ko

测试方法一:

直接给设备写入 1/0 来控制 LED 的亮灭
 sh -c 'echo 0 >/dev/led_chrdev0'
 sh -c 'echo 1 >/dev/led_chrdev0'

正点原子的LED是反的,所以1是亮,0是灭。

测试方法二:

./ledApp /dev/led 1 //打开 LED 灯
./ledApp /dev/led 0 //关闭 LED 灯

经实验,LED驱动工作正常。

卸载驱动:

rmmod led_cdev

如有侵权,或需要完整代码,请及时联系博主。

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

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

相关文章

每日一练:奇怪的TTL字段(python实现图片操作实战)

打开图片&#xff0c;只有四种数字&#xff1a;127&#xff0c;191&#xff0c;63&#xff0c;255 最大数字为255&#xff0c;想到进制转换 将其均转换为二进制&#xff1a; 发现只有前2位不一样 想着把每个数的前俩位提取出来&#xff0c;组成新的二进制&#xff0c;然后每…

Python中的数据容器及其在大数据开发中的应用

在Python编程中&#xff0c;数据容器是存储和组织数据的基本工具。作为大数据开发者&#xff0c;了解并灵活运用各种容器类型对于高效处理大规模数据至关重要。今天&#xff0c;我们将从Set出发&#xff0c;探讨Python中的各种数据容器&#xff0c;以及它们在大数据处理中的应用…

社交App iOS审核中的4.3问题:深入分析与解决策略

社交App审核中的4.3问题&#xff1a;深入分析与解决策略 在iOS应用开发和审核过程中&#xff0c;开发者经常会遇到苹果审核4.3问题。这一问题往往涉及应用的设计和内容重复性&#xff0c;导致应用被拒绝上架。为了帮助开发者更好地理解和解决这一问题&#xff0c;本文将对4.3问…

基于复旦微JFMQL100TAI的全国产化FPGA+AI人工智能异构计算平台,兼容XC7Z045-2FFG900I

基于上海复旦微电子FMQL45T900的全国产化ARM核心板。该核心板将复旦微的FMQL45T900&#xff08;与XILINX的XC7Z045-2FFG900I兼容&#xff09;的最小系统集成在了一个87*117mm的核心板上&#xff0c;可以作为一个核心模块&#xff0c;进行功能性扩展&#xff0c;能够快速的搭建起…

C语言操作符优先级

1 C语言操作符优先级 熟悉操作符的优先级&#xff0c;避免意外的求值顺序。 2. 运算符优先级记忆方法 利用优先级表或常见记忆口诀来记忆运算符的优先级。

嵌入式人工智能应用-篇外-烧写说明

1 外部接线 1.1 前期准备 需要准备的工具 ⚫ 一根 Mini USB 线 ⚫ 嵌入式人工智能教学科研平台 ⚫ 12V DC 电源 ⚫ 一台电脑 1.2 接线 12V DC 电源接入 12V IN&#xff1b;Mini USB 线连接 USB OTG&#xff1b;如果有两条 Mini USB 线&#xff0c;可以接入 UART2 to USB 口…

python2

一、条件语句 具体有如下&#xff1a;if、if......elif、if......elif......else 注意格式&#xff1a; if后面的条件表达式没有&#xff08;&#xff09;&#xff0c;以&#xff1a;作为结尾对于多分支的条件&#xff0c;不是写成else if 而是elif注意条件下一行要有缩进 …

Stable Diffusion 使用

目录 背景 最简单用法 进阶用法 高手用法 safetensor 一、概述 二、主要特点 背景 Stable Diffusion 开源后&#xff0c;确实比较火&#xff0c;上次介绍了下 Stable Diffusion 最简单的concept。今天继续介绍下&#xff0c;以Liblib 为例&#xff0c;介绍下如何使用参…

排序——交换排序

在上篇文章我们详细介绍了排序的概念与插入排序&#xff0c;大家可以通过下面这个链接去看&#xff1a; 排序的概念及插入排序 这篇文章就介绍一下一种排序方式&#xff1a;交换排序。 一&#xff0c;交换排序 基本思想&#xff1a;两两比较&#xff0c;如果发生逆序则交换…

Java | Leetcode Java题解之第234题回文链表

题目&#xff1a; 题解&#xff1a; class Solution {public boolean isPalindrome(ListNode head) {if (head null) {return true;}// 找到前半部分链表的尾节点并反转后半部分链表ListNode firstHalfEnd endOfFirstHalf(head);ListNode secondHalfStart reverseList(firs…

《BASeg: Boundary aware semantic segmentation for autonomous driving》论文解读

期刊&#xff1a;Neural Networks | Journal | ScienceDirect.com by Elsevier 年份&#xff1a;2023 代码&#xff1a;https://github.com/Lature-Yang/BASeg 摘要 语义分割是自动驾驶领域街道理解任务的重要组成部分。现有的各种方法要么专注于通过聚合全局或多尺度上下文…

读人工智能全传12人工智能导致的问题1

1. 人工智能会导致什么问题 1.1. 人工智能是一门通用技术&#xff1a;它的应用仅仅受限于我们的想象 1.1.1. 所有的技术都可能产生意想不到的效果&#xff0c;未来几十年甚至几百年内都存在可能性 1.2. 所有的技术都可能被滥用 1.2.1. 我们的无名氏祖先率先用上了火&#x…

C#统一委托Func与Action

C#在System命名空间下提供两个委托Action和Func&#xff0c;这两个委托最多提供16个参数&#xff0c;基本上可以满足所有自定义事件所需的委托类型。几乎所有的 事件 都可以使用这两个内置的委托Action和Func进行处理。 Action委托&#xff1a; Action定义提供0~16个参数&…

【深度学习】PyTorch深度学习笔记01-Overview

参考学习&#xff1a;B站视频【《PyTorch深度学习实践》完结合集】-刘二大人 ------------------------------------------------------------------------------------------------------- 1. 基于规则的深度学习 2. 经典的机器学习——手动提取一些简单的特征 3. 表示学习…

【接口设计】为 APP、PC、H5 网页提供统一风格的 API(实战篇,附源码地址)

为 APP、PC、H5 网页提供统一风格的 API 1.实现文章实体2.实现数据持久层3.实现服务接口和服务接口的实现类3.1 创建服务接口3.2 编写服务接口的实现 4.处理返回结果4.1 实现响应的枚举类4.2 实现返回的对象实体4.3 封装返回结果 4.统一处理异常4.1 全局捕捉异常4.2 自定义异常…

【防火墙】防火墙安全策略用户认证综合实验

实验拓扑及要求 拓扑搭建及IP配置 防火墙&#xff08;总公司&#xff09;和交换机&#xff08;汇聚生产区和办公区&#xff09;的接口配置 生产区在vlan2&#xff0c;办公区在vlan3&#xff0c;防火墙在G1/0/3接口上创建子接口G1/0/3.1和G1/0/3.2对两个区域分别进行管理 交换…

时间轮算法理解、Kafka实现

概述 TimingWheel&#xff0c;时间轮&#xff0c;简单理解就是一种用来存储若干个定时任务的环状队列&#xff08;或数组&#xff09;&#xff0c;工作原理和钟表的表盘类似。 关于环形队列&#xff0c;请参考环形队列。 时间轮由两个部分组成&#xff0c;一个环状数组&…

企业智能制造赋能的环境条件为什么重要?需要准备什么样的环境?

在全球制造业不断演进的今天&#xff0c;智能制造已经成为推动行业创新和转型的关键力量。它不仅代表了技术的革新&#xff0c;更是企业管理模式和运营思路的全面升级。然而&#xff0c;智能制造的落地实施并非一蹴而就&#xff0c;它需要企业在环境条件上做好充分的准备&#…

Study--Oracle-07-ASM自动存储管理(一)

一、ASM实例和数据库实例对应关系 1、ASM是Oracle 10g R2中为了简化Oracle数据库的管理而推出来的一项新功能&#xff0c;这是Oracle自己提供的卷管理器&#xff0c;主要用于替代操作系统所提供的LVM&#xff0c;它不仅支持单实例&#xff0c;同时对RAC的支持也是非常好。ASM可…

C语言 | Leetcode C语言题解之第231题2的幂

题目&#xff1a; 题解&#xff1a; const int BIG 1 << 30;bool isPowerOfTwo(int n) {return n > 0 && BIG % n 0; }