最近学习了Linux platform driver子系统和dts的基本知识,写一个platform字符设备驱动来操纵GPIO控制LED灯,采用DTB文件来提供硬件信息,纯粹练手。
修改DTS文件
要修改DTS文件首先要确定自己使用的开发板所用DTB的版本,RPI 4B采用的是bcm2711-rpi-4-b.dts。下载一个在arch/arm/boot/dts/目录下有这个dts文件的linux内核源码,我下的是linux-5.10.31。通过查阅bcm2711的datasheet,找到和GPIO相关的物理地址,RPI4B的GPIO物理地址偏移在0xfe200000。本实现希望的是通过读写GPIO14来控制外接的LED灯,所以需要使用的寄存器有:
0xfe200004 --- GPFSEL1
0xfe20001c --- GPSET0
0xfe200028 --- GPCLR0
于是在dts文件的根节点下添加节点:
dts_led {
#address-cells = <1>;
#size-cells = <1>;
compatible = "dts_led";
status = "okay";
reg = < 0xfe200000 0x04 /*BCM_GPIO_BASE*/
0xfe200004 0x04 /*GPFSEL1*/
0xfe20001c 0x04 /*GPSET0*/
0xfe200028 0x04 >; /*GPCLR0*/
};
编译需要自行配置交叉编译链,将编译得到的新的dtb替换原先的dtb。
若一切顺利,进入RPI 4B后在/proc/device-tree目录下可以看到dts_led节点。
编写驱动
驱动方面采用platform driver字符设备驱动,通过dtb文件的dts_led节点来获取硬件信息。
下面直接上代码
/*
面向RPI的LED驱动,对GPIO Pin 14的读写
platform driver版本 - dts
*/
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/uaccess.h> /*copy from ... copy to...*/
#include <linux/device.h> /*device_create class_create*/
#include <linux/slab.h> /*kzalloc kfree*/
#include <asm/io.h> /*readl writel ioremap*/
#include <linux/platform_device.h> /*platform_driver_register*/
#include <linux/of.h> /*struct device_node*/
#include <linux/of_address.h>
#define DTS_LED_MAJOR 233
#define GPFSEL1 (0x4/4)
#define GPSET0 (0x1c/4)
#define GPCLR0 (0x28/4)
static struct class *dts_led_class;
static struct device *dts_led_device;
static struct dts_led_dev *dts_led_devp;
static dev_t devno;
struct dts_led_dev {
unsigned int __iomem *bcm_gpio_base;
unsigned int *gpfsel1;
unsigned int *gpset0;
unsigned int *gpclr0;
struct device_node *nd; /*存放设备节点信息*/
struct cdev cdev;
};
/*修改GPFSEL1,将GPIO Pin14设置为output功能*/
static int dts_led_open(struct inode *inode, struct file *filp){
unsigned int sel1_value = 0;
sel1_value = readl(dts_led_devp->gpfsel1);
/*将sel1寄存器的12-14位修改为001*/
sel1_value |= (1<<12);
sel1_value &= ~(1<<13);
sel1_value &= ~(1<<14);
writel(sel1_value, dts_led_devp->gpfsel1);
printk(KERN_NOTICE "dts_led_open sel1_value = 0x%x\r\n", sel1_value);
return 0;
}
static ssize_t dts_led_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos){
return 0;
}
static ssize_t dts_led_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos){
unsigned char databuf[1];
if(copy_from_user(databuf, buf, size)){ // 读取用户空间写入的数据
printk(KERN_NOTICE "Failed to write platform_led!\r\n");
return -EFAULT;
}
unsigned char ledstat = databuf[0];
unsigned int set0_value = readl(dts_led_devp->gpset0);
unsigned int clr0_value = readl(dts_led_devp->gpclr0);
if(ledstat == 1){
/*打开LED*/
set0_value |= (1<<14);
writel(set0_value, dts_led_devp->gpset0);
}
else{
/*关闭LED*/
clr0_value |= (1<<14);
writel(clr0_value, dts_led_devp->gpclr0);
}
return 0;
}
struct file_operations dts_led_fops = {
.owner = THIS_MODULE,
.open = dts_led_open,
.read = dts_led_read,
.write = dts_led_write,
};
static int dts_led_probe(struct platform_device *pdev){
printk(KERN_NOTICE "%s", "dts_led load!\r\n");
int dts_led_major = DTS_LED_MAJOR;
devno = MKDEV(dts_led_major, 0);
int ret = 0;
if(dts_led_major){
ret = register_chrdev_region(devno, 1, "dts_led"); // 若platform_led的设备号已写死,向
内核申请devno设备号,名为platform_led
}
else{
ret = alloc_chrdev_region(&devno, 0, 1, "dts_led"); // 若reg_led的设备号需要内核分配
dts_led_major = MAJOR(devno);
}
if(ret<0){
printk(KERN_NOTICE "Failed to assign the devno!\r\n");
return ret;
}
dts_led_devp = kzalloc(sizeof(struct dts_led_dev), GFP_KERNEL);
if(dts_led_devp == NULL){
printk(KERN_NOTICE "Failed to assign mem for dts_led!\r\n");
ret = -ENOMEM;
goto fail;
}
/*从dtb文件读取硬件信息*/
/*1. 获取设备节点*/
dts_led_devp->nd = of_find_node_by_path("/dts_led");
if(dts_led_devp->nd == NULL){
printk(KERN_NOTICE "dts_led node can not found!\r\n");
return -EINVAL;
}
else{
printk(KERN_NOTICE "dts_led node found successfully!\r\n");
}
u32 regdata[8];
/*2.获取reg属性内容*/
ret = of_property_read_u32_array(dts_led_devp->nd, "reg", regdata, 8);
if(ret<0){
printk(KERN_NOTICE "reg property read Failed!\r\n");
goto fail;
}
/*将物理地址映射到虚拟地址*/
dts_led_devp->gpfsel1 = ioremap(regdata[2], regdata[3]);
dts_led_devp->gpset0 = ioremap(regdata[4], regdata[5]);
dts_led_devp->gpclr0 = ioremap(regdata[6], regdata[7]);
/*现在已经成功获得有效的devno和cdev了,下面进行cdev的注册*/
cdev_init(&dts_led_devp->cdev, &dts_led_fops);
dts_led_devp->cdev.owner = THIS_MODULE;
ret = cdev_add(&dts_led_devp->cdev, devno, 1);
if(ret){
printk(KERN_NOTICE "Failed to register the cdev to the kernel!\r\n");
goto fail;
}
/*现在完成了设备的注册,下面进行class和device的创建*/
dts_led_class = class_create(THIS_MODULE, "dts_led");
dts_led_device = device_create(dts_led_class, NULL, devno, NULL, "dts_led0");
return 0;
fail:
unregister_chrdev_region(devno, 1);
kfree(dts_led_devp);
return ret;
}
static int dts_led_remove(struct platform_device *dev){
device_destroy(dts_led_class, devno);
class_unregister(dts_led_class);
class_destroy(dts_led_class);
cdev_del(&dts_led_devp->cdev);
unregister_chrdev_region(devno, 1);
kfree(dts_led_devp);
printk(KERN_NOTICE "dts_led exit!\r\n");
return 0;
}
static const struct of_device_id dts_led_of_match[] = {
{.compatible = "dts_led"},
};
static struct platform_driver platform_led_driver = {
.driver = {
.name = "dts_led",
.of_match_table = dts_led_of_match,
},
.probe = dts_led_probe,
.remove = dts_led_remove,
};
static int __init dts_led_driver_init(void){
return platform_driver_register(&platform_led_driver);
}
static void __exit dts_led_driver_exit(void){
platform_driver_unregister(&platform_led_driver);
}
module_init(dts_led_driver_init);
module_exit(dts_led_driver_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Alex Tang");
测试程序
打开dts_led0设备文件进行读写,写入1将打开LED,写入0将关闭LED
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char* argv[]){
if(argc < 2){
fprintf(stderr, "Usage: %s [0/1]\n", argv[0]);
exit(EXIT_FAILURE);
}
int fd = open("/dev/dts_led0", O_RDWR, S_IRUSR|S_IWUSR);
unsigned char databuf[1];
databuf[0] = atoi(argv[1]);
if(write(fd, databuf, sizeof(databuf))){
fprintf(stderr, "write dts_led Failed!\n");
exit(EXIT_FAILURE);
}
return 0;
}