1、起因
学习内核驱动的过程中,希望进一步掌握字符设备框架;基于此,简单做一个可以从应用层调用,经过内核层,并在硬件层呈现流水灯(三个LED灯分别接在 Pin 12/13/19 )效果的小实验。
2、关键点分析
(1)通过阅读Tegra_X1芯片手册,确定所有需要写寄存器的地址(这里要注意板子上的引脚标号与芯片上的标号的对应关系,进而才能找到引脚所对应的控制寄存器)。这里需要写的寄存器有:引脚复用控制寄存器(配置目标引脚的功能为GPIO)和GPIO控制寄存器(包括CNF/OE/OUT等,主要用于配置该组GPIO8个引脚中的某个具体引脚);
(2)硬件层的地址如何在内核层中被使用?需要用到内核所提供的ioremap
函数,该函数的输入为:硬件起始地址 和 映射范围大小,返回值为 映射后得到的虚拟地址;
(3)当内核层与硬件层的通道被打通后,下面考虑应用层如何到内核层。我们知道在linux中,一切设备皆为文件,将LED设备看做为文件,对应一些操作为:
- 1)注册设备;
- 2)将字符设备与文件操作绑定;
- 3)将字符设备注册到内核中;
- 4)在应用层使用
mknod /dev/led c 500 0
在 Linux 系统中创建一个设备文件/dev/led
,并将其与字符设备类型(c
)以及指定的设备号(主设备号 500,次设备号 0)关联;
3、流水灯代码(编译后生成led.ko)
(1) led.c
文件
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/io.h>
#define LED_MA 500 //主设备号,区分不同设备类别
#define LED_MI 0 //次设备号
#define LED_MAGIC 'L'
#define LED0_ON _IOW(LED_MAGIC, 0, int) //使用幻术定义一个对应 LED0 的唯一操作数
#define LED0_OFF _IOW(LED_MAGIC, 1, int)
#define LED1_ON _IOW(LED_MAGIC, 2, int)
#define LED1_OFF _IOW(LED_MAGIC, 3, int)
#define LED2_ON _IOW(LED_MAGIC, 4, int)
#define LED2_OFF _IOW(LED_MAGIC, 5, int)
#define PINMUX_AUX_DAP4_SCLK_0 0x70003150 //管脚复用寄存器地址(12引脚)
#define PINMUX_AUX_SPI2_SCK_0 0x7000306c //定义第二个灯 管脚复用寄存器地址(13引脚)
#define PINMUX_AUX_SPI1_MOSI_0 0x70003050 //定义第三个灯 管脚复用寄存器地址(19引脚)
#define GPIO1 0x6000D000 //定义GPIO1寄存器的基地址
#define GPIO3 0x6000D200 //定义GPIO3寄存器的基地址
#define CNF 0x04 //下面几个宏 定义了J组GPIO引脚的基本配置寄存器偏移地址(C组和J组相同)
#define OE 0x14
#define OUT 0x24
#define MSK_CNF 0x84
#define MSK_OE 0x94
#define MSK_OUT 0xA4
#define CNF_ 0x08 //下面几个宏 定义了C组GPIO引脚的基本配置寄存器偏移地址
#define OE_ 0x18
#define OUT_ 0x28
#define MSK_CNF_ 0x88
#define MSK_OE_ 0x98
#define MSK_OUT_ 0xA8
struct cdev led_zhan; //声明字符设备
unsigned char *gpio_base0; //声明字符型指针,用来存放 ioremap()的返回值,gpio_base0 对应第一个灯的gpio控制器的基地址
unsigned char *gpio_pinmux0; //gpio_pinmux0 对应第一个灯的 管脚复用控制器的地址
unsigned char *gpio_base1;
unsigned char *gpio_pinmux1;
unsigned char *gpio_base2;
unsigned char *gpio_pinmux2;
int led_open(struct inode* innod, struct file * file) //打印模块插入提示信息
{
printk("led is opened\n ");
return 0;
}
int led_release(struct inode * innod, struct file * file) //打印模块卸载提示信息
{
printk("led is closed\n");
return 0;
}
static long led_ioctl(struct file *file, unsigned int cmd, unsigned long arg)//文件定制操作,分别表示各个灯的开关
{
switch(cmd){
case LED0_ON:
writel(readl(gpio_base0+OUT) | 1 << 7, gpio_base0+OUT); //指定板子上12引脚输出高电平
printk("LED0 state is ON! \n");
break;
case LED0_OFF:
writel(readl(gpio_base0+OUT) & ~(1 << 7), gpio_base0+OUT); //指定输出低电平
printk("LED0 state is OFF! \n");
break;
case LED1_ON:
writel(readl(gpio_base1+OUT) | 1 << 6, gpio_base1+OUT); //指定板子上13引脚输出高电平
printk("LED1 state is ON! \n");
break;
case LED1_OFF:
writel(readl(gpio_base1+OUT) & ~(1 << 6), gpio_base1+OUT); //指定输出低电平
printk("LED1 state is OFF! \n");
break;
case LED2_ON:
writel(readl(gpio_base2+OUT_) | 1 << 0, gpio_base2+OUT_); //指定板子上19引脚输出高电平
printk("LED2 state is ON! \n");
break;
case LED2_OFF:
writel(readl(gpio_base2+OUT_) & ~(1 << 0), gpio_base2+OUT_); //指定输出低电平
printk("LED2 state is OFF! \n");
break;
default:
printk("no found this cmd!!!\n");
}
return 0;
}
//声明文件操作,并进行关联
struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.release = led_release,
.unlocked_ioctl = led_ioctl,
};
static int led_init(void) //模块插入后,首先进入的函数
{
int ret, ret_1;
int devnum = MKDEV(LED_MA, LED_MI); //合并主次设备号,生成新的32位设备ID,避免资源浪费
/*下面调用内核函数来注册设备号,
parameter_1 : 起始设备编号;
parameter_2 : 请求的连续设备编号数
parameter_2 : 这个设备或驱动的名字*/
ret = register_chrdev_region(devnum, 1, "zhan_char_dev");
if(ret < 0) //添加异常判断
{
printk("register_chrdev_region failed! \n");
return ret;
}
//初始化字符设备(init 和 add 函数)
cdev_init(&led_zhan,&led_fops); //将字符设备与文件操作绑定
ret_1 = cdev_add(&led_zhan, devnum, 1); //将字符设备注册到内核中
if(ret_1 < 0) //添加异常判断
{
printk("cdev_add failed! \n");
goto err1;
}
//动态映射物理地址到内核虚拟地址,返回值
gpio_pinmux0 = ioremap(PINMUX_AUX_DAP4_SCLK_0, 8);//指定管脚复用寄存器地址,‘8’为映射范围大小,单位字节
if(gpio_pinmux0 == NULL){
printk("ioremap gpio_pinmux0 error!\n");
goto err2;
}
writel((readl(gpio_pinmux0)& ~(1<<4)) | 1, gpio_pinmux0);
gpio_base0 = ioremap(GPIO3,0xFF);
if(gpio_base0 == NULL){
printk("ioremap gpio_base0 error! \n");
goto err3;
}
writel(readl(gpio_base0+CNF) | 1 << 7, gpio_base0+CNF); //配置引脚GPIO3_PJ.07 为 GPIO模式
writel(readl(gpio_base0+OE) | 1 << 7, gpio_base0+OE); //使能引脚(7号)
writel(readl(gpio_base0+OUT) | ~(1 << 7), gpio_base0+OUT); //输出低电平,保持初始状态为:灯不亮
writel(readl(gpio_base0+MSK_CNF) | 1 << 7, gpio_base0+MSK_CNF); //取消对GPIO模下引脚的屏蔽
writel(readl(gpio_base0+MSK_OE) | 1 << 7, gpio_base0+MSK_OE); //取消引脚 使能屏蔽
//
gpio_pinmux1 = ioremap(PINMUX_AUX_SPI2_SCK_0, 8);//指定管脚复用寄存器地址,‘8’为映射范围大小,单位字节
if(gpio_pinmux1 == NULL){
printk("ioremap gpio_pinmux1 error!\n");
goto err4;
}
writel((readl(gpio_pinmux1)& ~(1<<4)) | 1, gpio_pinmux1);
gpio_base1 = ioremap(GPIO1,0xFF);
if(gpio_base1 == NULL){
printk("ioremap gpio_base1 error! \n");
goto err5;
}
writel(readl(gpio_base1+CNF) | 1 << 6, gpio_base1+CNF); //配置引脚GPIO1_PB.06 为 GPIO模式
writel(readl(gpio_base1+OE) | 1 << 6, gpio_base1+OE); //使能引脚(6号)
writel(readl(gpio_base1+OUT) | ~(1 << 6), gpio_base1+OUT); //输出低电平,保持初始状态为:灯不亮
writel(readl(gpio_base1+MSK_CNF) | 1 << 6, gpio_base1+MSK_CNF); //取消对GPIO模下引脚的屏蔽
writel(readl(gpio_base1+MSK_OE) | 1 << 6, gpio_base1+MSK_OE); //取消引脚 使能屏蔽
gpio_pinmux2 = ioremap(PINMUX_AUX_SPI1_MOSI_0, 8);//指定管脚复用寄存器地址,‘8’为映射范围大小,单位字节
if(gpio_pinmux2 == NULL){
printk("ioremap gpio_pinmux2 error!\n");
goto err6;
}
writel((readl(gpio_pinmux2)& ~(1<<4)) | 1, gpio_pinmux2);
gpio_base2 = ioremap(GPIO1,0xFF);
if(gpio_base2 == NULL){
printk("ioremap gpio_base2 error! \n");
goto err7;
}
writel(readl(gpio_base2+CNF_) | 1 << 0, gpio_base2+CNF_); //配置引脚GPIO3_PY.02 为 GPIO模式
writel(readl(gpio_base2+OE_) | 1 << 0, gpio_base2+OE_); //使能引脚(2号)
writel(readl(gpio_base2+OUT_) | ~(1 << 0), gpio_base2+OUT_); //输出低电平,保持初始状态为:灯不亮
writel(readl(gpio_base2+MSK_CNF_) | 1 << 0, gpio_base2+MSK_CNF_); //取消对GPIO模下引脚的屏蔽
writel(readl(gpio_base2+MSK_OE_) | 1 << 0, gpio_base2+MSK_OE_); //取消引脚 使能屏蔽
printk("led init_1_24 is done\n");
return 0;
err7: //顺序申请,逆序释放
iounmap(gpio_pinmux2);
err6:
iounmap(gpio_base1);
err5:
iounmap(gpio_pinmux1);
err4:
iounmap(gpio_base0);
err3:
iounmap(gpio_pinmux0);
err2:
cdev_del(&led_zhan);
err1:
unregister_chrdev_region(devnum, 1);
return -1;
}
static void led_exit(void)
{
int devnum = MKDEV(LED_MA, LED_MI);
cdev_del(&led_zhan); //和字符设备添加相配对
unregister_chrdev_region(devnum, 1);//卸载设备时,需要取消注册,释放资源(与注册行为配对)
printk("led exit_1 is done\n");
}
module_init(led_init);//模块安装入口声明
module_exit(led_exit); //模块卸载入口声明
MODULE_LICENSE("GPL"); //模块免费开源声明
MODULE_AUTHOR("Chanlgin"); //模块作者声明(可选)
(2)Makefile
ifeq ($(KERNELRELEASE),)
KERNELDIR ?= ~/kernel-4.9
PWD := $(shell pwd)
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions Module* modules* a.out
else
obj-m := led.o
endif
(3)app.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#define LED_MAGIC 'L' //幻数
#define LED0_ON _IOW(LED_MAGIC, 0, int) //使用幻术定义和LED对应唯一操作数
#define LED0_OFF _IOW(LED_MAGIC, 1, int)
#define LED1_ON _IOW(LED_MAGIC, 2, int) //使用幻术定义和LED对应唯一操作数
#define LED1_OFF _IOW(LED_MAGIC, 3, int)
#define LED2_ON _IOW(LED_MAGIC, 4, int) //使用幻术定义和LED对应唯一操作数
#define LED2_OFF _IOW(LED_MAGIC, 5, int)
int main(int argc, char **argv)
{
int fd;
fd = open("/dev/led", O_RDWR); //打开设备文件
if (fd < 0) {
perror("open");
exit(1);
}
while(1)
{
ioctl(fd, LED0_ON); //发送控制命令 LED0_ON
usleep(500000);
ioctl(fd, LED0_OFF); //发送控制命令 LED0_OFF
usleep(500000);
ioctl(fd, LED1_ON); //发送控制命令 LED1_ON
usleep(500000);
ioctl(fd, LED1_OFF); //发送控制命令 LED1_OFF
usleep(500000);
ioctl(fd, LED2_ON); //发送控制命令 LED2_ON
usleep(500000);
ioctl(fd, LED2_OFF); //发送控制命令 LED2_OFF
usleep(500000);
}
return 0;
}
4、测试
$ make
$ aarch64-linux-gnu-gcc app.c //编译应用程序,生成a.out
$ cp led.ko a.out /nfs/rootfs
# insmod led.ko
# mknod /dev/led c 500 0 //创建设备文件
#./a.out
5、效果
终端打印信息:
LED闪烁效果:
VID20250121222225