若该文为原创文章,转载请注明原文出处。
字符设备驱动程序的基本框架,主要是如何申请及释放设备号、添加以及注销设备,初始化、添加与删除 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 ,所以要操作的地址为基地址+偏移地址=0xFDC20010GPIO 的基地址为 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
如有侵权,或需要完整代码,请及时联系博主。