《【北京迅为】itop-3568开发板驱动开发指南.pdf》 学习笔记
文章目录
ioremap 物理地址映射
通过开发板原理图得知 LED 控制引脚为 GPIO0_B7,IO 高电平时,三极管导通,LED 点亮,IO 低电平时,三极管截止,LED 熄灭,
1. IO 复用寄存器
复用寄存器用来配置引脚功能,在芯片手册搜索 GPIO0_B7,可以知晓它的引脚功能由复用寄存器 PMU_GRF_GPIO0B_IOMUX_H 决定,[14:12] 为 0h 时,该引脚为 GPIO 功能,
通过查看 PMU_GRF_GPIO0B_IOMUX_H 的寄存器描述,发现其默认值为 0x00000000,即 GPIO0_B7 默认为 GPIO 功能,所以无需修改该寄存器的值,
2. 数据方向寄存器
下面是 rk3568 所有 GPIO 寄存器,不过只有前四个是我们要用到的(数据寄存器和数据方向寄存器),
上表中并没有告知方向寄存器和数据寄存器的基地址,我们可以在芯片手册的 Address Mapping 菜单获取它们的基地址,
rk3568 有四组 GPIO(GPIOA、GPIOB、GPIOC、GPIOD),每组有各有 8 个(A0~A7…)GPIO,所以 GPIO0_B7 的数据方向由 GPIO_SWPORT_DDR_L 寄存器决定,该寄存器的低 16 位用于控制 IO 方向,高 16 位用于控制 IO 写使能。(低 16 位的写使能)
GPIO0_B7 在 GPIO0 和 GPIO1 两组 IO 中排第 16(从 1 算起) ,如果要将 GPIO0_B7 设置为输出模式,需要将 GPIO_SWPORT_DDR_L 的 15 位和 31 位设置为 1。
3. 数据寄存器
数据寄存器与数据方向寄存器类似,我们需要将 GPIO_SWPORT_DR_L 31 位设置为 1([15:0] 的写使能),同时通过该寄存器的第 15 位来控制 IO 电平(1 为高电平),
4 .实验程序
代码参考原文档
驱动核心代码:
通过 ioremap() ,可以将底层物理地址映射到内核虚拟地址,这样便能向操作单片机(寄存器)一样操作 arm-linux 开发板(寄存器)了,
// 将物理地址转化为虚拟地址(寄存器为 32 位,所以映射 4 字节)
dev1.gpio_ddr = ioremap(GPIO_REG_BASE + GPIO_DDR_REG, 4); // IO 数据方向
if(IS_ERR(dev1.gpio_ddr))
{
ret = PTR_ERR(dev1.gpio_ddr); // 返回错误码
goto err_ddr_ioremap;
}
// 1000 0000 1000 0000
*(dev1.gpio_ddr) |= 0x80008000; // GPIO0_B7 设置输出模式
完整驱动代码:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#define GPIO_REG_BASE 0xFDD60000// gpio 寄存器基地址
#define GPIO_DDR_REG 0x0008 // 数据方向寄存器偏移地址
#define GPIO_DR_REG 0x0000 // 数据寄存器偏移地址
// 定义一个私有数据结构体
struct my_device
{
dev_t dev_num; // 设备号
int major; // 主设备号
int minor; // 次设备号
struct cdev st_cdev;
struct class *st_class;
struct device *st_device;
char kbuf[32];
unsigned int *gpio_ddr; // io 数据方向
unsigned int *gpio_dr; // io 高低电平
// 地址数据类型千万不能写小了!!!
};
// 定义一个全局私有数据结构体
struct my_device dev1;
// open()
static int chrdev_open(struct inode *inode , struct file *file )
{
file->private_data = &dev1; // 设置私有数据
printk("chrdev_open.\n");
return 0;
}
// close()
static int chrdev_release(struct inode *inode, struct file *file)
{
printk("chrdev_release.\n");
return 0;
}
// write()
static ssize_t chrdev_write(struct file *file , const char __user *buf, size_t size, loff_t *off)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
unsigned int tmp;
int ret = copy_from_user(tmp_dev->kbuf, buf, size); // 从应用空间读取数据
if(ret != 0)
{
printk("copy_from_user error.\r\n");
return -1;
}
if(tmp_dev->kbuf[0] == 1) // 如果读到 1,点亮 LED
{
*(tmp_dev->gpio_dr) |= 0x80008000; // GPIO0_B7 高电平
}
else if(tmp_dev->kbuf[0] == 0) // 读到 0,熄灭 LED
{
tmp = *(dev1.gpio_dr) | 0x80000000; // 数据寄存器使能(中间值)
*(dev1.gpio_dr) = tmp & ~(0x00008000); // GPIO0_B7 低电平
// 实测:数据寄存器写使能必须和 IO 电平值同时写入寄存器,所以这里用到了中间变量
}
return 0;
}
// read()
static ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
printk("chrdev_read.\n");
return 0;
}
static struct file_operations chrdev_fops = {
.owner = THIS_MODULE, //将 owner 成员指向本模块,可以避免在模块的操作正在被使用时卸载该模块
.open = chrdev_open, //将 open 成员指向 chrdev_open()函数
.read = chrdev_read, //将 read 成员指向 chrdev_read()函数
.write = chrdev_write,//将 write 字段指向 chrdev_write()函数
.release = chrdev_release,//将 release 字段指向 chrdev_release()函数
};
// 驱动入口函数
static int __init chrdev_init(void)
{
int ret;
// 自动获取设备号(只申请一个,次设备号从 0 开始)
ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "chrdev_test");
if(ret < 0)
{
goto err_alloc;
}
printk("alloc chrdev region successfully.\n");
dev1.major = MAJOR(dev1.dev_num); // 获取主设备号
dev1.minor = MINOR(dev1.dev_num); // 获取次设备号
printk("major is %d.\nminor is %d\n", dev1.major, dev1.minor);
dev1.st_cdev.owner = THIS_MODULE; // 将 owner 成员指向本模块,可以避免模块 st_cdev 被使用时卸载模块
cdev_init(&dev1.st_cdev, &chrdev_fops); // 初始化字符设备
ret = cdev_add(&dev1.st_cdev, dev1.dev_num, 1); // 将字符设备添加到系统
if(ret < 0)
{
goto err_cdev_add;
}
printk("cdev add successfully.\n");
dev1.st_class = class_create(THIS_MODULE, "chrdev_class"); // 创建设备类
if(IS_ERR(dev1.st_class))
{
ret = PTR_ERR(dev1.st_class); // 返回错误码
goto err_class_create;
}
dev1.st_device = device_create(dev1.st_class, NULL, dev1.dev_num, NULL, "chrdev_device"); // 创建设备
if(IS_ERR(dev1.st_device))
{
ret = PTR_ERR(dev1.st_device); // 返回错误码
goto err_device_create;
}
// 将物理地址转化为虚拟地址(寄存器为 32 位,所以映射 4 字节)
dev1.gpio_ddr = ioremap(GPIO_REG_BASE + GPIO_DDR_REG, 4); // IO 数据方向
if(IS_ERR(dev1.gpio_ddr))
{
ret = PTR_ERR(dev1.gpio_ddr); // 返回错误码
goto err_ddr_ioremap;
}
// 1000 0000 1000 0000
*(dev1.gpio_ddr) |= 0x80008000; // GPIO0_B7 设置输出模式
dev1.gpio_dr = ioremap(GPIO_REG_BASE + GPIO_DR_REG, 4); // IO 电平
if(IS_ERR(dev1.gpio_dr))
{
ret = PTR_ERR(dev1.gpio_dr); // 返回错误码
goto err_dr_ioremap;
}
return 0;
err_dr_ioremap:
iounmap(dev1.gpio_dr); // 取消映射
err_ddr_ioremap:
iounmap(dev1.gpio_ddr); // 取消映射
err_device_create:
class_destroy(dev1.st_class); // 删除类
err_class_create:
cdev_del(&dev1.st_cdev); // 删除 cdev
err_cdev_add:
unregister_chrdev_region(dev1.dev_num, 1); // 注销设备号
err_alloc:
return ret; // 返回错误号
}
// 驱动出口函数
static void __exit chrdev_exit(void)
{
iounmap(dev1.gpio_dr); // 取消映射
iounmap(dev1.gpio_ddr); // 取消映射
device_destroy(dev1.st_class, dev1.dev_num); // 删除设备
class_destroy(dev1.st_class); //删除设备类
cdev_del(&dev1.st_cdev); // 删除字符设备
unregister_chrdev_region(dev1.dev_num, 1); // 注销设备号
printk("chrdev_exit.\n");
}
module_init(chrdev_init); //注册入口函数
module_exit(chrdev_exit); //注册出口函数
MODULE_LICENSE("GPL v2"); //同意GPL协议
MODULE_AUTHOR("xiaohui"); //作者信息
app 代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define DEV_FILE "/dev/chrdev_device"
int main(int argc, char** argv)
{
int fd, tmp;
int ret = 0;
// 打开设备文件
fd = open(DEV_FILE, O_RDWR);
if(fd < 0)
{
printf("%s open failed.\n", DEV_FILE);
return 0;
}
printf("%s open successfully.\n", DEV_FILE);
// 调用 write(),写 1 点亮 LED,写 0 熄灭 LED.
while(1)
{
tmp = 1;
write(fd, &tmp, 1);
sleep(1);
tmp = 0;
write(fd, &tmp, 1);
sleep(1);
}
// 关闭设备文件
close(fd);
return 0;
}
Makefile
#目标文件,与驱动源文件同名,编译成模块
obj-m := chrdev_test.o
#架构平台选择
export ARCH=arm64
#编译器选择
export CROSS_COMPILE=aarch64-linux-gnu-
#内核目录
KDIR := /home/topeet/Linux/rk356x_linux/kernel/
#KDIR := /lib/modules/$(shell uname -r)/build
#编译模块
all:
make -C $(KDIR) M=$(shell pwd) modules
$(CROSS_COMPILE)gcc app.c -o app
#清除编译文件
clean:
make -C $(KDIR) M=$(shell pwd) clean
rm app
测试结果:
串口终端安装驱动,然后执行测试程序,
开发板效果:
调用 API
物理地址映射的方式可以让我们对 linux 开发板点灯有一个较全面的认识,但操作比较复杂,且移植性不好,下面我们试着使用调用 API 的方式控制 LED(GPIO),
只是简单地调用 Linux GPIO API,不作深入了解
先提一下 GPIO 编号,到网上找资料发现,RK3568 的 GPIO 编号很好计算,以 GPIO0_B7 为例,其 bank 为 0,group 为 1(A 为 0,以此类推),X 为 7,那么它的 pin 编号为 bank * 32 + group + X = 15,直白点讲,GPIO 的编号就是它在所有 IO 中所属序号,
// GPIO0_B7 (bank = 0, group = 1(B), X = 7, pin = 0 * 32 + 1 * 8 + 7 = 15)
#define LED_GPIO 15 // LED GPIO 编号
关键代码1,请求 gpio 和设置 gpio 模式:
ret = gpio_request(LED_GPIO, "LED9"); // 请求 IO 资源
if(ret < 0)
{
goto err_gpio_request;
}
printk("gpio request successfully.\n");
ret = gpio_direction_output(LED_GPIO, 0); // 设置为输出模式
if(ret < 0)
{
goto err_gpio_dir_out;
}
printk("set gpio output mode successfully.\n");
关键代码2,设置 gpio 电平
if(tmp_dev->kbuf[0] == 1) // 如果读到 1,点亮 LED
{
gpio_set_value(LED_GPIO, 1); // LED_GPIO 拉高
}
else if(tmp_dev->kbuf[0] == 0) // 读到 0,熄灭 LED
{
gpio_set_value(LED_GPIO, 0); // LED_GPIO 拉低
}
完整驱动代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/gpio.h>
// GPIO0_B7 (bank = 0, group = 1(B), X = 7, pin = 0 * 32 + 1 * 8 + 7 = 15)
#define LED_GPIO 15 // LED GPIO 编号
// 定义一个私有数据结构体
struct my_device
{
dev_t dev_num; // 设备号
int major; // 主设备号
int minor; // 次设备号
struct cdev st_cdev;
struct class *st_class;
struct device *st_device;
char kbuf[32];
};
// 定义一个全局私有数据结构体
struct my_device dev1;
// open()
static int chrdev_open(struct inode *inode , struct file *file )
{
file->private_data = &dev1; // 设置私有数据
printk("chrdev_open.\n");
return 0;
}
// close()
static int chrdev_release(struct inode *inode, struct file *file)
{
printk("chrdev_release.\n");
return 0;
}
// write()
static ssize_t chrdev_write(struct file *file , const char __user *buf, size_t size, loff_t *off)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
int ret = copy_from_user(tmp_dev->kbuf, buf, size); // 从应用空间读取数据
if(ret != 0)
{
printk("copy_from_user error.\r\n");
return -1;
}
if(tmp_dev->kbuf[0] == 1) // 如果读到 1,点亮 LED
{
gpio_set_value(LED_GPIO, 1); // LED_GPIO 拉高
}
else if(tmp_dev->kbuf[0] == 0) // 读到 0,熄灭 LED
{
gpio_set_value(LED_GPIO, 0); // LED_GPIO 拉低
}
return 0;
}
// read()
static ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
printk("chrdev_read.\n");
return 0;
}
static struct file_operations chrdev_fops = {
.owner = THIS_MODULE, //将 owner 成员指向本模块,可以避免在模块的操作正在被使用时卸载该模块
.open = chrdev_open, //将 open 成员指向 chrdev_open()函数
.read = chrdev_read, //将 read 成员指向 chrdev_read()函数
.write = chrdev_write,//将 write 字段指向 chrdev_write()函数
.release = chrdev_release,//将 release 字段指向 chrdev_release()函数
};
// 驱动入口函数
static int __init chrdev_init(void)
{
int ret;
// 自动获取设备号(只申请一个,次设备号从 0 开始)
ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "chrdev_test");
if(ret < 0)
{
goto err_alloc;
}
printk("alloc chrdev region successfully.\n");
dev1.major = MAJOR(dev1.dev_num); // 获取主设备号
dev1.minor = MINOR(dev1.dev_num); // 获取次设备号
printk("major is %d.\nminor is %d\n", dev1.major, dev1.minor);
dev1.st_cdev.owner = THIS_MODULE; // 将 owner 成员指向本模块,可以避免模块 st_cdev 被使用时卸载模块
cdev_init(&dev1.st_cdev, &chrdev_fops); // 初始化字符设备
ret = cdev_add(&dev1.st_cdev, dev1.dev_num, 1); // 将字符设备添加到系统
if(ret < 0)
{
goto err_cdev_add;
}
printk("cdev add successfully.\n");
dev1.st_class = class_create(THIS_MODULE, "chrdev_class"); // 创建设备类
if(IS_ERR(dev1.st_class))
{
ret = PTR_ERR(dev1.st_class); // 返回错误码
goto err_class_create;
}
dev1.st_device = device_create(dev1.st_class, NULL, dev1.dev_num, NULL, "chrdev_device"); // 创建设备
if(IS_ERR(dev1.st_device))
{
ret = PTR_ERR(dev1.st_device); // 返回错误码
goto err_device_create;
}
ret = gpio_is_valid(LED_GPIO); // 检查 gpio 是否可用
if(ret < 0)
{
goto err_gpio_valid;
}
printk("%d: gpio is valid.\n", LED_GPIO);
ret = gpio_request(LED_GPIO, "LED9"); // 请求 IO 资源
if(ret < 0)
{
goto err_gpio_request;
}
printk("gpio request successfully.\n");
ret = gpio_direction_output(LED_GPIO, 0); // 设置为输出模式
if(ret < 0)
{
goto err_gpio_dir_out;
}
printk("set gpio output mode successfully.\n");
return 0;
err_gpio_dir_out:
gpio_free(LED_GPIO); // 释放 GPIO
err_gpio_request:
err_gpio_valid:
// 无操作
err_device_create:
class_destroy(dev1.st_class); // 删除类
err_class_create:
cdev_del(&dev1.st_cdev); // 删除 cdev
err_cdev_add:
unregister_chrdev_region(dev1.dev_num, 1); // 注销设备号
err_alloc:
return ret; // 返回错误号
}
// 驱动出口函数
static void __exit chrdev_exit(void)
{
gpio_free(LED_GPIO); // 释放 GPIO
device_destroy(dev1.st_class, dev1.dev_num); // 删除设备
class_destroy(dev1.st_class); //删除设备类
cdev_del(&dev1.st_cdev); // 删除字符设备
unregister_chrdev_region(dev1.dev_num, 1); // 注销设备号
printk("chrdev_exit.\n");
}
module_init(chrdev_init); //注册入口函数
module_exit(chrdev_exit); //注册出口函数
MODULE_LICENSE("GPL v2"); //同意GPL协议
MODULE_AUTHOR("xiaohui"); //作者信息
应用代码、Makefile 及实物效果与上一个实验相同,这里不重复展示,下面是串口终端上的运行结果,