前言
前面学习和了解了字符设备和文件系统是怎么关联起来的,这篇文章就来实践一下,加深前面的理解。我们写一个基本的GPIO引脚驱动用于驱动板载的LED灯和蜂鸣器,并在文件系统里面创建字符设备文件节点,用于控制GPIO引脚。
驱动代码
c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
// 提供struct cdev结构体定义
#include <linux/cdev.h>
// 提供dev_t定义
#include <linux/types.h>
// 提供struct file_operations定义
#include <linux/fs.h>
// 提供ioremap相关定义
#include <asm/io.h>
// 提供字符串比较函数
#include <linux/string.h>
// 提供copy_from_user函数
#include <asm/uaccess.h>
// 提供sysfs_streq函数
#include <linux/device.h>
/* 原理图信息
LED: GPIO1_IO03, Low-Triggerd
BEEP: GPIO5_IO01, Low-Triggerd
*/
/* 寄存器设置
开启GPIO时钟:
LED: CCGR1 0x020C_406C [CG13]=b[27:26]=0b_11, GPIO1_CLK_ENABLE
BEEP: CCGR1 0x020C_406C [CG15]=b[31:30]=0b_11, GPIO5_CLK_ENABLE
0b_00 - 所有模式下关闭时钟
0b_01 - 仅在运行模式(Run mode)下打开
0b_10 - 保留
0b_11 - 在除了停止模式(Stop mode)外的所有模式下都打开
引脚复用为GPIO:
LED: SW_MUX_CTL_PAD_GPIO1_IO03 0x020E_0068 b[3:0]=0b_0101, GPIO1_IO03
BEEP: SW_MUX_CTL_PAD_SNVS_TAMPER1 0x0229_000C b[3:0]=0b_0101, GPIO5_IO01
引脚电气属性:
LED: SW_PAD_CTL_PAD_GPIO1_IO03 0x020E_02F4 default=0x000010B0
BEEP: SW_PAD_CTL_PAD_SNVS_TAMPER1 0x0229_0050 default=0x000110A0
GPIO方向:
LED: GPIO1_GDIR 0x0209_C004 b[3]=0b_1, OUTPUT
BEEP: GPIO5_GDIR 0x020A_C004 b[1]=0b_1, OUTPUT
GPIO电平:
LED: GPIO1_DR 0x0209_C000 b[3]
BEEP: GPIO5_DR 0x020A_C000 b[1]
*/
#define GPIO_NUM (2)
typedef struct
{
u32 reg_addr_clk; // GPIO的时钟使能寄存器
int reg_bit_clk; // GPIO引脚的时钟使能位偏移
u32 reg_addr_iomux; // GPIO引脚复用寄存器
u32 reg_addr_pad; // GPIO引脚电气属性寄存器
u32 pad_default; // GPIO引脚默认电气属性
u32 reg_addr_gdir; // GPIO引脚方向寄存器
int reg_bit_gdir; // GPIO引脚的方向位偏移
u32 reg_addr_dr; // GPIO引脚电平值寄存器
int reg_bit_dr; // GPIO引脚电平值位偏移
int active_level; // GPIO引脚的有效电平
} gpio_info_t;
typedef struct
{
void __iomem *vir_addr_clk;
void __iomem *vir_addr_iomux;
void __iomem *vir_addr_pad;
void __iomem *vir_addr_gdir;
void __iomem *vir_addr_dr;
} gpio_vir_addr_t;
typedef struct
{
dev_t gpio_dev;
struct cdev gpio_cdev;
} chrdev_info_t;
const gpio_info_t gpios[GPIO_NUM] =
{
{
.reg_addr_clk = 0x020C406C,
.reg_bit_clk = 26,
.reg_addr_iomux = 0x020E0068,
.reg_addr_pad = 0x020E02F4,
.pad_default = 0x000010B0,
.reg_addr_gdir = 0x0209C004,
.reg_bit_gdir = 3,
.reg_addr_dr = 0x0209C000,
.reg_bit_dr = 3,
.active_level = 0,
},
{
.reg_addr_clk = 0x020C406C,
.reg_bit_clk = 30,
.reg_addr_iomux = 0x0229000C,
.reg_addr_pad = 0x02290050,
.pad_default = 0x000110A0,
.reg_addr_gdir = 0x020AC004,
.reg_bit_gdir = 1,
.reg_addr_dr = 0x020AC000,
.reg_bit_dr = 1,
.active_level = 0,
}
};
gpio_vir_addr_t vir_addrs[GPIO_NUM] = { { NULL}, { NULL } };
chrdev_info_t gpio_chrdev;
static int init_gpios(void)
{
int i;
u32 val;
for (i = 0; i < GPIO_NUM; i++)
{
vir_addrs[i].vir_addr_clk = ioremap(gpios[i].reg_addr_clk, 4);
vir_addrs[i].vir_addr_iomux = ioremap(gpios[i].reg_addr_iomux, 4);
vir_addrs[i].vir_addr_pad = ioremap(gpios[i].reg_addr_pad, 4);
vir_addrs[i].vir_addr_gdir = ioremap(gpios[i].reg_addr_gdir, 4);
vir_addrs[i].vir_addr_dr = ioremap(gpios[i].reg_addr_dr, 4);
if (!vir_addrs[i].vir_addr_clk || !vir_addrs[i].vir_addr_iomux ||
!vir_addrs[i].vir_addr_pad || !vir_addrs[i].vir_addr_gdir ||
!vir_addrs[i].vir_addr_dr)
goto error;
}
for (i = 0; i < GPIO_NUM; i++)
{
// 使能GPIO口时钟
val = readl(vir_addrs[i].vir_addr_clk);
val &= ~(0x3 << gpios[i].reg_bit_clk);
writel(val | (0x3 << gpios[i].reg_bit_clk), vir_addrs[i].vir_addr_clk);
// 设置引脚复用为GPIO
writel(0x5, vir_addrs[i].vir_addr_iomux);
// 恢复引脚的默认电气属性
writel(gpios[i].pad_default, vir_addrs[i].vir_addr_pad);
// 设置GPIO为输出
val = readl(vir_addrs[i].vir_addr_gdir);
val &= ~(0x1 << gpios[i].reg_bit_gdir);
writel(val | (0x1 << gpios[i].reg_bit_gdir), vir_addrs[i].vir_addr_gdir);
// 输出无效电平
val = readl(vir_addrs[i].vir_addr_dr);
val &= ~(0x1 << gpios[i].reg_bit_dr);
writel(val | ((gpios[i].active_level ? 0x0 : 0x1) << gpios[i].reg_bit_dr), vir_addrs[i].vir_addr_dr);
}
return 0;
error:
for (i = 0; i < GPIO_NUM; i++)
{
if (vir_addrs[i].vir_addr_clk)
{
iounmap(vir_addrs[i].vir_addr_clk);
vir_addrs[i].vir_addr_clk = NULL;
}
if (vir_addrs[i].vir_addr_iomux)
{
iounmap(vir_addrs[i].vir_addr_iomux);
vir_addrs[i].vir_addr_iomux = NULL;
}
if (vir_addrs[i].vir_addr_pad)
{
iounmap(vir_addrs[i].vir_addr_pad);
vir_addrs[i].vir_addr_pad = NULL;
}
if (vir_addrs[i].vir_addr_gdir)
{
iounmap(vir_addrs[i].vir_addr_gdir);
vir_addrs[i].vir_addr_gdir = NULL;
}
if (vir_addrs[i].vir_addr_dr)
{
iounmap(vir_addrs[i].vir_addr_dr);
vir_addrs[i].vir_addr_dr = NULL;
}
}
return -1;
}
static void deinit_gpios(void)
{
int i;
u32 val;
for (i = 0; i < GPIO_NUM; i++)
{
// 输出无效电平
val = readl(vir_addrs[i].vir_addr_dr);
val &= ~(0x1 << gpios[i].reg_bit_dr);
writel(val | ((gpios[i].active_level ? 0x0 : 0x1) << gpios[i].reg_bit_dr), vir_addrs[i].vir_addr_dr);
}
for (i = 0; i < GPIO_NUM; i++)
{
// 释放页表
iounmap(vir_addrs[i].vir_addr_clk);
iounmap(vir_addrs[i].vir_addr_iomux);
iounmap(vir_addrs[i].vir_addr_pad);
iounmap(vir_addrs[i].vir_addr_gdir);
iounmap(vir_addrs[i].vir_addr_dr);
vir_addrs[i].vir_addr_clk = NULL;
vir_addrs[i].vir_addr_iomux = NULL;
vir_addrs[i].vir_addr_pad = NULL;
vir_addrs[i].vir_addr_gdir = NULL;
vir_addrs[i].vir_addr_dr = NULL;
}
}
static void set_gpio_value(int index, int value)
{
u32 val;
val = readl(vir_addrs[index].vir_addr_dr);
val &= ~(0x1 << gpios[index].reg_bit_dr);
writel(val | ((value ? 0x1 : 0x0) << gpios[index].reg_bit_dr), vir_addrs[index].vir_addr_dr);
}
static int gpio_open(struct inode *inode, struct file *file)
{
int minor = MINOR(inode->i_rdev);
if (minor >= GPIO_NUM)
{
printk(KERN_ERR "invalid file node\n");
return -1;
}
return 0;
}
static int gpio_release(struct inode * inode, struct file * file)
{
int minor = MINOR(inode->i_rdev);
if (minor >= GPIO_NUM)
{
printk(KERN_ERR "invalid file node\n");
return -1;
}
return 0;
}
static ssize_t gpio_write(struct file *file, const char __user *buffer, size_t count, loff_t *ppos)
{
char databuf[32] = { '\0' };
size_t datalen = 0;
int minor = MINOR(file->f_inode->i_rdev);
if (minor >= GPIO_NUM)
{
printk(KERN_ERR "invalid file node\n");
return -1;
}
datalen = count >= sizeof(databuf) - 1 ? sizeof(databuf) - 1 : count;
if (copy_from_user(databuf, buffer, datalen))
{
printk(KERN_ERR "copy from user failed\n");
return -1;
}
if (sysfs_streq(databuf, "ON"))
{
set_gpio_value(minor, gpios[minor].active_level ? 1 : 0);
}
else if (sysfs_streq(databuf, "OFF"))
{
set_gpio_value(minor, gpios[minor].active_level ? 0 : 1);
}
else
{
printk(KERN_WARNING "unsupported command: %s\n", databuf);
}
return count;
}
static const struct file_operations gpio_fops =
{
.open = gpio_open,
.release = gpio_release,
.write = gpio_write,
};
static int __init gpio_basic_init(void)
{
int ret;
// 申请设备号
ret = alloc_chrdev_region(&gpio_chrdev.gpio_dev, 0, GPIO_NUM, "gpios");
if (ret < 0)
{
printk(KERN_ERR "can't allocate dev number\n");
return -1;
}
// 设置字符设备驱动对象,添加操作函数集
gpio_chrdev.gpio_cdev.owner = THIS_MODULE;
cdev_init(&gpio_chrdev.gpio_cdev, &gpio_fops);
// 注册字符设备驱动,绑定设备号与字符设备驱动对象
cdev_add(&gpio_chrdev.gpio_cdev, gpio_chrdev.gpio_dev, GPIO_NUM);
// 初始化GPIO
if (init_gpios() != 0)
goto error;
return 0;
error:
cdev_del(&gpio_chrdev.gpio_cdev);
unregister_chrdev_region(gpio_chrdev.gpio_dev, GPIO_NUM);
return -1;
}
static void __exit gpio_basic_exit(void)
{
deinit_gpios();
cdev_del(&gpio_chrdev.gpio_cdev);
unregister_chrdev_region(gpio_chrdev.gpio_dev, GPIO_NUM);
}
module_init(gpio_basic_init);
module_exit(gpio_basic_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("David");
MODULE_DESCRIPTION("basic gpio driver");
MODULE_ALIAS("Basic GPIO");
上一篇文章我们总结了要加载字符设备驱动应该做些什么工作,这里补充具体调用了什么函数来完成这些工作:
- 我们要创建一个代表字符设备驱动的
struct cdev结构体,因为这个结构体会在其它地方被引用,所以它的生命周期为设备驱动存续期间,应该定义成一个字符设备驱动里的一个全局变量或者在堆内存上分配空间,这里我们就是将其定义在全局结构体gpio_chrdev里面。 - 定义字符设备操作函数集合
struct file_operations结构体,其生命周期也为设备驱动存续期间,这里我们就将其定义成一个全局变量gpio_fops,并实现了其中的open,release和write成员。 - 从内核里为这个字符设备驱动分配一个主设备号,并指定其下管理多少个子设备,内核会将分配的设备号信息记录到
chrdevs哈希表中。这里我们使用int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)函数让内核为我们自动分配一个主设备号,并指定次设备数量为想要控制的GPIO引脚数量。 - 将字符设备操作函数集合
struct file_operations指定给字符设备驱动对象struct cdev,这里使用函数void cdev_init(struct cdev *cdev, const struct file_operations *fops)。 - 往内核里注册这个字符设备驱动对象
struct cdev并指定主设备号dev,内核会在probes哈希表里添加主设备号dev和字符设备驱动对象struct cdev的映射。这里使用函数int cdev_add(struct cdev *p, dev_t dev, unsigned count)。
Makefile
bash
KERNEL_DIR := /home/david/share/driver-uboot-linux/linux
ARCH := arm
CROSS_COMPILE := arm-linux-gnueabihf-
export ARCH CROSS_COMPILE
module_name := gpio_basic
install_dir := /home/david/share/nfs/rootfs/modules/$(module_name)
obj-m := $(module_name).o
.PHONY: all clean install
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
install: all
@if [ ! -d "$(install_dir)" ]; then \
echo "Creating $(install_dir) directory..."; \
mkdir -p $(install_dir); \
fi
cp *.ko $(install_dir)
加载驱动模块
bash
insmod gpio_basic.ko
查看主设备号如下:
bash
/modules/gpio_basic # cat /proc/devices | grep gpio
248 gpios
创建字符设备文件节点
bash
mknod /dev/led c 248 0
mknod /dev/beep c 248 1
测试
bash
echo ON > /dev/led
echo OFF > /dev/led
echo ON > /dev/beep
echo OFF > /dev/beep
经验证可以正常控制LED灯和蜂鸣器。
删除设备文件节点
bash
rm /dev/led
rm /dev/beep
卸载驱动模块
bash
rmmod gpio_basic