目前尚不是最终版本,还望有心人自己学习的时候,把自己整合的知识点相关的答案也好问题也好,或者实践过程中的一些操作截图,再或者其他的一些想要分享材料发给笔者邮箱:[email protected],我们一起完善这篇博客!笔者写这篇博客的时候已经工作第四个年头了,目前是在整理之前有过的学习资料,仅作为笔记,供同志们参考!短时间内可能不会去全部完善。
还是那个硬件,硬件物理原理不变,硬件操作接口(寄存器)不变,硬件操作代码不变,哪里不同了?
寄存器地址不同。原来是直接用物理地址,现在需要用该物理地址在内核虚拟地址空间相对应的虚拟地址。寄存器的物理地址是CPU设计时决定的,从datasheet中查找到的。
编程方法不同。裸机中习惯直接用函数指针操作寄存器地址,而kernel中习惯用封装好的io读写函数来操作寄存器,以实现最大程度可移植性。
内核的虚拟地址映射方法?为什么需要虚拟地址映射?
内核中有2套虚拟地址映射方法:动态和静态
静态映射方法的特点:内核移植时以代码的形式硬编码,如果要更改必须改源代码后重新编译内核,在内核启动时建立静态映射表,到内核关机时销毁,中间一直有效,对于移植好的内核,你用不用他都在那里。
动态映射方法的特点:驱动程序根据需要随时动态的建立映射、使用、销毁映射,映射是短期临时的。
如何选择虚拟地址映射方法?
2种映射并不排他,可以同时使用;静态映射类似于C语言中全局变量,动态方式类似于C语言中malloc堆内存;静态映射的好处是执行效率高,坏处是始终占用虚拟地址空间;动态映射的好处是按需使用虚拟地址空间,坏处是每次使用前后都需要代码去建立映射&销毁映射(还得学会使用那些内核函数的使用)。
关于静态映射要说的:不同版本内核中静态映射表位置、文件名可能不同,不同SoC的静态映射表位置、文件名可能不同,所谓映射表其实就是头文件中的宏定义。
三星版本内核中的静态映射表?
主映射表位于:arch/arm/plat-s5p/include/plat/map-s5p.h。
CPU在安排寄存器地址时不是随意乱序分布的,而是按照模块去区分的。每一个模块内部的很多个寄存器的地址是连续的。所以内核在定义寄存器地址时都是先找到基地址,然后再用基地址+偏移量来寻找具体的一个寄存器。map-s5p.h中定义的就是要用到的几个模块的寄存器基地址。
map-s5p.h中定义的是模块的寄存器基地址的虚拟地址。
虚拟地址基地址定义在:arch/arm/plat-samsung/include/plat/map-base.h。
#define S3C_ADDR_BASE (0xFD000000),三星移植时确定的静态映射表的基地址,表中的所有虚拟地址都是以这个地址+偏移量来指定的。
GPIO相关的主映射表位于:arch/arm/mach-s5pv210/include/mach/regs-gpio.h。
表中是GPIO的各个端口的基地址的定义;
GPIO的具体寄存器定义位于:arch/arm/mach-s5pv210/include/mach/gpio-bank.h。
参考裸机中的操作方法添加LED操作代码:宏定义?在init和exit函数中分别点亮和熄灭LED?实践测试?insmod和rmmod时观察LED亮灭变化?打印出寄存器的值和静态映射表中的分析相对比?将代码移动到open和close函数中去?
添加驱动中的写函数?
先定义好应用和驱动之间的控制接口,这个是由自己来定义的。譬如定义为:应用向驱动写"on"则驱动让LED亮,应用向驱动写"off",驱动就让LED灭;应用和驱动的接口定义做的尽量简单,譬如用1个字符来表示。譬如定义为:应用写"1"表示灯亮,写"0"表示让灯灭。写应用来测试写函数?驱动和应用中来添加读功能?
完整的相关代码见下述文件:
Makefile
# ubuntu的内核源码树,如果要编译在ubuntu中安装的模块就打开这2个
# KERN_VER = $(shell uname -r)
# KERN_DIR = /lib/modules/$(KERN_VER)/build
# 开发板的linux内核的源码树目录
KERN_DIR = /root/qt/kernel
obj-m += module_test.o
all:
make -C $(KERN_DIR) M=`pwd` modules
# arm-none-linux-gnueabi-gcc app.c -o app
cp:
cp *.ko /root/removal/rootfs/root/driver_test
# cp app /root/removal/rootfs/root/driver_test
.PHONY: clean
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
app.c
#include
#include
#include
#include
#include
#define FILE "/dev/test_chrdev"
char buf[100];
int main(void)
{
// 打开文件
int fd = -1;
int i = 0;
fd = open(FILE, O_RDWR);
if (fd < 0)
{
printf("open %s error.\n", FILE);
return -1;
}
printf("open %s success..\n", FILE);
// 读写文件
while (1)
{
memset(buf, 0 , sizeof(buf));
printf("请输入 on | off \n");
scanf("%s", buf);
if (!strcmp(buf, "on"))
{
write(fd, "1", 1);
}
else if (!strcmp(buf, "off"))
{
write(fd, "0", 1);
}
else if (!strcmp(buf, "flash"))
{
for (i=0; i<3; i++)
{
write(fd, "1", 1);
sleep(1);
write(fd, "0", 1);
sleep(1);
}
}
else if (!strcmp(buf, "quit"))
{
break;
}
}
// 关闭文件
close(fd);
return 0;
}
module_test.c
// 为了module_init,module_exit相关的,加入下面头文件
#include
// 为了__init,__exit相关的,加入下面头文件
#include
#include
#include
#include
// arch/arm/mach-s5pv210/include/mach/gpio-bank.h
#include
#include
// #define MYMAJOR 250
#define MYNAME "test_chrdev"
#define GPJ0CON S5PV210_GPJ0CON
#define GPJ0DAT S5PV210_GPJ0DAT
#define rGPJ0CON *((volatile unsigned int *)GPJ0CON)
#define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)
unsigned int *pGPJ0CON;
unsigned int *pGPJ0DAT;
int mymajor;
// 内核空间的buf
char kbuf[100];
static int test_chrdev_open(struct inode *inode, struct file *file)
{
// 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分
// 但是现在暂时我们写不了这么多,所以用一个printk打印个信息来意思意思。
printk(KERN_INFO "test_chrdev_open\n");
rGPJ0CON = 0x11111111;
// 三个灯亮
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
return 0;
}
static int test_chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "test_chrdev_release\n");
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
return 0;
}
ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "test_chrdev_read\n");
ret = copy_to_user(ubuf, kbuf, count);
if (ret)
{
printk(KERN_ERR "copy_to_user fail\n");
return -EINVAL;
}
printk(KERN_INFO "copy_to_user success..\n");
return 0;
}
// 写函数的本质就是将应用层传递过来的数据先复制到内核中,然后将之以正确的方式写入硬件完成操作。
static ssize_t test_chrdev_write(struct file *file, const char __user *ubuf,
size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "test_chrdev_write\n");
// 使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间中的一个buf中
//memcpy(kbuf, ubuf);不行,因为2个buf不在一个地址空间中
ret = copy_from_user(kbuf, ubuf, count);
if (ret)
{
printk(KERN_ERR "copy_from_user fail\n");
return -EINVAL;
}
printk(KERN_INFO "copy_from_user success..\n");
/*
// 真正的驱动中,数据从应用层复制到驱动中后,我们就要根据这个数据
// 去写硬件完成硬件的操作。所以这下面就应该是操作硬件的代码
if (!strcmp(kbuf, "on"))
{
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
}
else if (!strcmp(kbuf, "off"))
{
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
}
*/
if (kbuf[0] == '1')
{
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
}
else if (kbuf[0] == '0')
{
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
}
return 0;
}
// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops = {
// 惯例,直接写即可
.owner = THIS_MODULE,
// 将来应用open打开这个设备时实际调用的
// 就是这个.open对应的函数
.open = test_chrdev_open,
.release = test_chrdev_release,
.write = test_chrdev_write,
.read = test_chrdev_read,
};
// 模块安装函数
static int __init chrdev_init(void)
{
printk(KERN_INFO "chrdev_init helloworld init\n");
// 在module_init宏调用的函数中去注册字符设备驱动
// major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号
// 内核如果成功分配就会返回分配的主设备号;如果分配失败会返回负数
mymajor = register_chrdev(0, MYNAME, &test_fops);
if (mymajor < 0)
{
printk(KERN_ERR "register_chrdev fail\n");
return -EINVAL;
}
printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);
/*
// 模块安装命令insmod时执行的硬件操作
rGPJ0CON = 0x11111111;
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
printk(KERN_INFO "GPJ0CON = %p.\n", GPJ0CON);
printk(KERN_INFO "GPJ0DAT = %p.\n", GPJ0DAT);
*/
return 0;
}
// 模块卸载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit helloworld exit\n");
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
// 在module_exit宏调用的函数中去注销字符设备驱动
unregister_chrdev(mymajor, MYNAME);
}
module_init(chrdev_init);
module_exit(chrdev_exit);
// MODULE_xxx这种宏作用是用来添加模块描述信息
// 描述模块的许可证
MODULE_LICENSE("GPL");
// 描述模块的作者
MODULE_AUTHOR("aston");
// 描述模块的介绍信息
MODULE_DESCRIPTION("module test");
// 描述模块的别名信息
MODULE_ALIAS("alias xxx");
如何建立动态映射?
request_mem_region,向内核申请(报告)需要映射的内存资源。
ioremap,真正用来实现映射,传给他物理地址他给你映射返回一个虚拟地址。
如何销毁动态映射?
iounmap?
release_mem_region?
注意:映射建立时,是要先申请再映射;然后使用;使用完要解除映射时要先解除映射再释放申请。
代码实践?2个寄存器分开独立映射?2个寄存器在一起映射?
完整的相关代码见下述文件(其他未列出的文件等同于首次列出的完整代码中的文件):
module_test.c
// 为了module_init,module_exit相关的,加入下面头文件
#include
// 为了__init,__exit相关的,加入下面头文件
#include
#include
#include
#include
// arch/arm/mach-s5pv210/include/mach/gpio-bank.h
#include
#include
#include
#include
// #define MYMAJOR 250
#define MYNAME "test_chrdev"
// #define GPJ0CON S5PV210_GPJ0CON
// #define GPJ0DAT S5PV210_GPJ0DAT
// #define rGPJ0CON *((volatile unsigned int *)GPJ0CON)
// #define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)
#define GPJ0CON_PA 0xe0200240
#define GPJ0DAT_PA 0xe0200244
unsigned int *pGPJ0CON;
unsigned int *pGPJ0DAT;
int mymajor;
// 内核空间的buf
char kbuf[100];
static int test_chrdev_open(struct inode *inode, struct file *file)
{
// 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分
// 但是现在暂时我们写不了这么多,所以用一个printk打印个信息来意思意思。
printk(KERN_INFO "test_chrdev_open\n");
// rGPJ0CON = 0x11111111;
*pGPJ0CON = 0x11111111;
// 三个灯亮
//rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
return 0;
}
static int test_chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "test_chrdev_release\n");
// rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
*pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
return 0;
}
ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "test_chrdev_read\n");
ret = copy_to_user(ubuf, kbuf, count);
if (ret)
{
printk(KERN_ERR "copy_to_user fail\n");
return -EINVAL;
}
printk(KERN_INFO "copy_to_user success..\n");
return 0;
}
// 写函数的本质就是将应用层传递过来的数据先复制到内核中,然后将之以正确的方式写入硬件完成操作。
static ssize_t test_chrdev_write(struct file *file, const char __user *ubuf,
size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "test_chrdev_write\n");
// 使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间中的一个buf中
//memcpy(kbuf, ubuf);不行,因为2个buf不在一个地址空间中
ret = copy_from_user(kbuf, ubuf, count);
if (ret)
{
printk(KERN_ERR "copy_from_user fail\n");
return -EINVAL;
}
printk(KERN_INFO "copy_from_user success..\n");
/*
// 真正的驱动中,数据从应用层复制到驱动中后,我们就要根据这个数据
// 去写硬件完成硬件的操作。所以这下面就应该是操作硬件的代码
if (!strcmp(kbuf, "on"))
{
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
}
else if (!strcmp(kbuf, "off"))
{
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
}
*/
if (kbuf[0] == '1')
{
// rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
}
else if (kbuf[0] == '0')
{
// rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
*pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
}
return 0;
}
// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops = {
// 惯例,直接写即可
.owner = THIS_MODULE,
// 将来应用open打开这个设备时实际调用的
// 就是这个.open对应的函数
.open = test_chrdev_open,
.release = test_chrdev_release,
.write = test_chrdev_write,
.read = test_chrdev_read,
};
// 模块安装函数
static int __init chrdev_init(void)
{
printk(KERN_INFO "chrdev_init helloworld init\n");
// 在module_init宏调用的函数中去注册字符设备驱动
// major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号
// 内核如果成功分配就会返回分配的主设备号;如果分配失败会返回负数
mymajor = register_chrdev(0, MYNAME, &test_fops);
if (mymajor < 0)
{
printk(KERN_ERR "register_chrdev fail\n");
return -EINVAL;
}
printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);
// 使用动态映射的方式来操作寄存器
if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
return -EINVAL;
if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0DAT"))
return -EINVAL;
pGPJ0CON = ioremap(GPJ0CON_PA, 4);
pGPJ0DAT = ioremap(GPJ0DAT_PA, 4);
*pGPJ0CON = 0x11111111;
*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
/*
// 模块安装命令insmod时执行的硬件操作
rGPJ0CON = 0x11111111;
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
printk(KERN_INFO "GPJ0CON = %p.\n", GPJ0CON);
printk(KERN_INFO "GPJ0DAT = %p.\n", GPJ0DAT);
*/
return 0;
}
// 模块卸载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit helloworld exit\n");
// rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
*pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
// 解除映射
iounmap(pGPJ0CON);
iounmap(pGPJ0DAT);
release_mem_region(GPJ0CON_PA, 4);
release_mem_region(GPJ0DAT_PA, 4);
// 在module_exit宏调用的函数中去注销字符设备驱动
unregister_chrdev(mymajor, MYNAME);
}
module_init(chrdev_init);
module_exit(chrdev_exit);
// MODULE_xxx这种宏作用是用来添加模块描述信息
// 描述模块的许可证
MODULE_LICENSE("GPL");
// 描述模块的作者
MODULE_AUTHOR("aston");
// 描述模块的介绍信息
MODULE_DESCRIPTION("module test");
// 描述模块的别名信息
MODULE_ALIAS("alias xxx");
值得注意,只在注册和卸载驱动模块的地方用了动态映射,如需要所有相关的操作都使用动态映射,请自行修改。
建立映射表的三个关键部分:
映射表中具体的物理地址和虚拟地址的值相关的宏定义;
映射表建立函数?该函数负责由映射表来建立linux内核的页表映射关系;
源码分析?为了加深功力可以去看相关内核源码,这里仅以分析出来的主要函数调用关系作为简单记录:
映射表建立函数(kernel/arch/arm/mach-s5pv210/mach-smdkc110.c):
smdkc110_map_io
s5p_init_io
iotable_init
结论:经过分析,真正的静态映射表是在内核移植时给定的,于arch/arm/plat-s5p/cpu.c中的s5p_iodesc,本质是一个结构体数组,数组中每一个元素就是一个映射,这个映射描述了一段物理地址到虚拟地址之间的映射关系。这个结构体数组所记录的几个映射关系被iotable_init所使用,该函数负责将这个结构体数组格式的表建立成MMU所能识别的页表映射关系,这样在开机后可以直接使用相对应的虚拟地址来访问对应的物理地址。
开机时调用映射表建立函数:
开机时(kernel启动时)smdkc110_map_io怎么被调用的?
源码分析?为了加深功力可以去看相关内核源码,这里仅以分析出来的主要函数调用关系作为简单记录:
调用过程:
start_kernel
setup_arch
paging_init
devicemaps_init
代码片段:
if (mdesc->map_io)
mdesc->map_io();
问题描述:
仿效真实驱动中,用结构体封装的方式来进行多寄存器的地址映射。来代替之前使用的多次映射。
实践编码?分析和总结?
完整的相关代码见下述文件(其他未列出的文件等同于首次列出的完整代码中的文件):
module_test.c
// 为了module_init,module_exit相关的,加入下面头文件
#include
// 为了__init,__exit相关的,加入下面头文件
#include
#include
#include
#include
// arch/arm/mach-s5pv210/include/mach/gpio-bank.h
#include
#include
#include
#include
#include
#include
// #define MYMAJOR 250
#define MYCNT 1
#define MYNAME "test_chrdev"
// #define GPJ0CON S5PV210_GPJ0CON
// #define GPJ0DAT S5PV210_GPJ0DAT
// #define rGPJ0CON *((volatile unsigned int *)GPJ0CON)
// #define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)
// #define GPJ0CON_PA 0xe0200240
// #define GPJ0DAT_PA 0xe0200244
#define GPJ0_REGBASE 0xe0200240
// unsigned int *pGPJ0CON;
// unsigned int *pGPJ0DAT;
typedef struct GPJ0REG
{
volatile unsigned int gpj0con;
volatile unsigned int gpj0dat;
}gpj0_reg_t;
gpj0_reg_t *pGPJ0REG;
// int mymajor;
static dev_t mydev;
// static struct cdev test_cdev;
static struct cdev *pcdev;
static struct class *test_class;
// 内核空间的buf
char kbuf[100];
static int test_chrdev_open(struct inode *inode, struct file *file)
{
// 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分
// 但是现在暂时我们写不了这么多,所以用一个printk打印个信息来意思意思。
printk(KERN_INFO "test_chrdev_open\n");
// rGPJ0CON = 0x11111111;
// *pGPJ0CON = 0x11111111;
pGPJ0REG->gpj0con = 0x11111111;
// 三个灯亮
//rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
// *pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
pGPJ0REG->gpj0dat = ((0<<3) | (0<<4) | (0<<5));
return 0;
}
static int test_chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "test_chrdev_release\n");
// rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
// *pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
pGPJ0REG->gpj0dat = ((1<<3) | (1<<4) | (1<<5));
return 0;
}
ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "test_chrdev_read\n");
ret = copy_to_user(ubuf, kbuf, count);
if (ret)
{
printk(KERN_ERR "copy_to_user fail\n");
return -EINVAL;
}
printk(KERN_INFO "copy_to_user success..\n");
return 0;
}
// 写函数的本质就是将应用层传递过来的数据先复制到内核中,然后将之以正确的方式写入硬件完成操作。
static ssize_t test_chrdev_write(struct file *file, const char __user *ubuf,
size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "test_chrdev_write\n");
// 使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间中的一个buf中
//memcpy(kbuf, ubuf);不行,因为2个buf不在一个地址空间中
memset(kbuf, 0, sizeof(kbuf));
ret = copy_from_user(kbuf, ubuf, count);
if (ret)
{
printk(KERN_ERR "copy_from_user fail\n");
return -EINVAL;
}
printk(KERN_INFO "copy_from_user success..\n");
/*
// 真正的驱动中,数据从应用层复制到驱动中后,我们就要根据这个数据
// 去写硬件完成硬件的操作。所以这下面就应该是操作硬件的代码
if (!strcmp(kbuf, "on"))
{
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
}
else if (!strcmp(kbuf, "off"))
{
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
}
*/
if (kbuf[0] == '1')
{
// rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
// *pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
pGPJ0REG->gpj0dat = ((0<<3) | (0<<4) | (0<<5));
}
else if (kbuf[0] == '0')
{
// rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
// *pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
pGPJ0REG->gpj0dat = ((1<<3) | (1<<4) | (1<<5));
}
return 0;
}
// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops = {
// 惯例,直接写即可
.owner = THIS_MODULE,
// 将来应用open打开这个设备时实际调用的
// 就是这个.open对应的函数
.open = test_chrdev_open,
.release = test_chrdev_release,
.write = test_chrdev_write,
.read = test_chrdev_read,
};
// 模块安装函数
static int __init chrdev_init(void)
{
int retval;
printk(KERN_INFO "chrdev_init helloworld init\n");
/*
// 在module_init宏调用的函数中去注册字符设备驱动
// major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号
// 内核如果成功分配就会返回分配的主设备号;如果分配失败会返回负数
mymajor = register_chrdev(0, MYNAME, &test_fops);
if (mymajor < 0)
{
printk(KERN_ERR "register_chrdev fail\n");
return -EINVAL;
}
printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);
*/
// 使用新的cdev接口来注册字符设备驱动
// 新的接口注册字符设备驱动需要2步
// 第1步:注册/分配主次设备号
// mydev = MKDEV(MYMAJOR, 0);
// retval = register_chrdev_region(mydev, MYCNT, MYNAME);
retval = alloc_chrdev_region(&mydev, 12, MYCNT, MYNAME);
if (retval) {
// printk(KERN_ERR "Unable to register minors for %s\n", MYNAME);
// return -EINVAL;
printk(KERN_ERR "Unable to alloc minors for %s\n", MYNAME);
goto flag1;
}
// printk(KERN_INFO "register_chrdev_region success\n");
printk(KERN_INFO "alloc_chrdev_region success\n");
printk(KERN_INFO "major = %d, minor = %d.\n", MAJOR(mydev), MINOR(mydev));
// 第2步:注册字符设备驱动
// 给pcdev分配内存,指针实例化
pcdev = cdev_alloc();
// cdev_init(&test_cdev, &test_fops);
// cdev_init(pcdev, &test_fops);
pcdev->owner = THIS_MODULE;
pcdev->ops = &test_fops;
// retval = cdev_add(&test_cdev, mydev, MYCNT);
retval = cdev_add(pcdev, mydev, MYCNT);
if (retval) {
printk(KERN_ERR "Unable to cdev_add\n");
goto flag2;
}
printk(KERN_INFO "cdev_add success\n");
// 注册字符设备驱动完成后,添加设备类的操作,以让内核帮我们发信息
// 给udev,让udev自动创建和删除设备文件
test_class = class_create(THIS_MODULE, "aston_class");
if (IS_ERR(test_class))
return -EINVAL;
// 最后1个参数字符串,就是我们将来要在/dev目录下创建的设备文件的名字
// 所以我们这里要的文件名是/dev/test
device_create(test_class, NULL, mydev, NULL, "test_chrdev");
/*
// 使用动态映射的方式来操作寄存器
if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
// return -EINVAL;
goto flag3;
if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0DAT"))
// return -EINVAL;
goto flag3;
pGPJ0CON = ioremap(GPJ0CON_PA, 4);
pGPJ0DAT = ioremap(GPJ0DAT_PA, 4);
*pGPJ0CON = 0x11111111;
*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
*/
// 使用结构体和动态映射的方式来操作寄存器
if (!request_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t), "GPJ0REG"))
return -EINVAL;
pGPJ0REG = ioremap(GPJ0_REGBASE, sizeof(gpj0_reg_t));
// 映射之后用指向结构体的指针来进行操作
// 指针使用->结构体内元素的方式来操作各个寄存器
pGPJ0REG->gpj0con = 0x11111111;
pGPJ0REG->gpj0dat = ((0<<3) | (0<<4) | (0<<5));
/*
// 模块安装命令insmod时执行的硬件操作
rGPJ0CON = 0x11111111;
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
printk(KERN_INFO "GPJ0CON = %p.\n", GPJ0CON);
printk(KERN_INFO "GPJ0DAT = %p.\n", GPJ0DAT);
*/
// goto flag0:
return 0;
// 如果第4步才出错跳转到这里来
flag4:
// release_mem_region(GPJ0CON_PA, 4);
// release_mem_region(GPJ0DAT_PA, 4);
release_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t));
// 如果第3步才出错跳转到这里来
flag3:
// cdev_del(&test_cdev);
cdev_del(pcdev);
// 如果第2步才出错跳转到这里来
flag2:
// 在这里把第1步做成功的东西给注销掉
unregister_chrdev_region(mydev, MYCNT);
// 如果第1步才出错跳转到这里来
flag1:
return -EINVAL;
// flag0:
// return 0;
}
// 模块卸载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit helloworld exit\n");
// rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
// *pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
pGPJ0REG->gpj0dat = ((1<<3) | (1<<4) | (1<<5));
/*
// 解除映射
iounmap(pGPJ0CON);
iounmap(pGPJ0DAT);
release_mem_region(GPJ0CON_PA, 4);
release_mem_region(GPJ0DAT_PA, 4);
*/
// 解除映射
iounmap(pGPJ0REG);
release_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t));
device_destroy(test_class, mydev);
class_destroy(test_class);
/*
// 在module_exit宏调用的函数中去注销字符设备驱动
unregister_chrdev(mymajor, MYNAME);
*/
// 使用新的接口来注销字符设备驱动
// 注销分2步:
// 第一步真正注销字符设备驱动用cdev_del
// cdev_del(&test_cdev);
cdev_del(pcdev);
// 第二步去注销申请的主次设备号
unregister_chrdev_region(mydev, MYCNT);
}
module_init(chrdev_init);
module_exit(chrdev_exit);
// MODULE_xxx这种宏作用是用来添加模块描述信息
// 描述模块的许可证
MODULE_LICENSE("GPL");
// 描述模块的作者
MODULE_AUTHOR("aston");
// 描述模块的介绍信息
MODULE_DESCRIPTION("module test");
// 描述模块的别名信息
MODULE_ALIAS("alias xxx");
前面访问寄存器的方式:
行不行?好不好?
内核提供的寄存器读写接口:
writel和readl;
iowrite32和ioread32;
代码实践?分析和总结?
完整的相关代码见下述文件(其他未列出的文件等同于首次列出的完整代码中的文件):
module_test.c
// 为了module_init,module_exit相关的,加入下面头文件
#include
// 为了__init,__exit相关的,加入下面头文件
#include
#include
#include
#include
// arch/arm/mach-s5pv210/include/mach/gpio-bank.h
#include
#include
#include
#include
#include
#include
// #define MYMAJOR 250
#define MYCNT 1
#define MYNAME "test_chrdev"
// #define GPJ0CON S5PV210_GPJ0CON
// #define GPJ0DAT S5PV210_GPJ0DAT
// #define rGPJ0CON *((volatile unsigned int *)GPJ0CON)
// #define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)
// #define GPJ0CON_PA 0xe0200240
// #define GPJ0DAT_PA 0xe0200244
#define GPJ0_REGBASE 0xe0200240
#define S5P_GPJ0REG(x) (x)
#define S5P_GPJ0CON S5P_GPJ0REG(0)
#define S5P_GPJ0DAT S5P_GPJ0REG(4)
// unsigned int *pGPJ0CON;
// unsigned int *pGPJ0DAT;
// 寄存器的虚拟地址的基地址
static void __iomem *baseaddr;
/*
typedef struct GPJ0REG
{
volatile unsigned int gpj0con;
volatile unsigned int gpj0dat;
}gpj0_reg_t;
gpj0_reg_t *pGPJ0REG;
*/
// int mymajor;
static dev_t mydev;
// static struct cdev test_cdev;
static struct cdev *pcdev;
static struct class *test_class;
// 内核空间的buf
char kbuf[100];
static int test_chrdev_open(struct inode *inode, struct file *file)
{
// 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分
// 但是现在暂时我们写不了这么多,所以用一个printk打印个信息来意思意思。
printk(KERN_INFO "test_chrdev_open\n");
// rGPJ0CON = 0x11111111;
// *pGPJ0CON = 0x11111111;
// pGPJ0REG->gpj0con = 0x11111111;
writel(0x11111111, baseaddr + S5P_GPJ0CON);
// 三个灯亮
//rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
// *pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
// pGPJ0REG->gpj0dat = ((0<<3) | (0<<4) | (0<<5));
writel(((0<<3) | (0<<4) | (0<<5)), baseaddr + S5P_GPJ0DAT);
return 0;
}
static int test_chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "test_chrdev_release\n");
// rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
// *pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
// pGPJ0REG->gpj0dat = ((1<<3) | (1<<4) | (1<<5));
writel(((1<<3) | (1<<4) | (1<<5)), baseaddr + S5P_GPJ0DAT);
return 0;
}
ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "test_chrdev_read\n");
ret = copy_to_user(ubuf, kbuf, count);
if (ret)
{
printk(KERN_ERR "copy_to_user fail\n");
return -EINVAL;
}
printk(KERN_INFO "copy_to_user success..\n");
return 0;
}
// 写函数的本质就是将应用层传递过来的数据先复制到内核中,然后将之以正确的方式写入硬件完成操作。
static ssize_t test_chrdev_write(struct file *file, const char __user *ubuf,
size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "test_chrdev_write\n");
// 使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间中的一个buf中
//memcpy(kbuf, ubuf);不行,因为2个buf不在一个地址空间中
memset(kbuf, 0, sizeof(kbuf));
ret = copy_from_user(kbuf, ubuf, count);
if (ret)
{
printk(KERN_ERR "copy_from_user fail\n");
return -EINVAL;
}
printk(KERN_INFO "copy_from_user success..\n");
/*
// 真正的驱动中,数据从应用层复制到驱动中后,我们就要根据这个数据
// 去写硬件完成硬件的操作。所以这下面就应该是操作硬件的代码
if (!strcmp(kbuf, "on"))
{
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
}
else if (!strcmp(kbuf, "off"))
{
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
}
*/
if (kbuf[0] == '1')
{
// rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
// *pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
// pGPJ0REG->gpj0dat = ((0<<3) | (0<<4) | (0<<5));
writel(((0<<3) | (0<<4) | (0<<5)), baseaddr + S5P_GPJ0DAT);
}
else if (kbuf[0] == '0')
{
// rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
// *pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
// pGPJ0REG->gpj0dat = ((1<<3) | (1<<4) | (1<<5));
writel(((1<<3) | (1<<4) | (1<<5)), baseaddr + S5P_GPJ0DAT);
}
return 0;
}
// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops = {
// 惯例,直接写即可
.owner = THIS_MODULE,
// 将来应用open打开这个设备时实际调用的
// 就是这个.open对应的函数
.open = test_chrdev_open,
.release = test_chrdev_release,
.write = test_chrdev_write,
.read = test_chrdev_read,
};
// 模块安装函数
static int __init chrdev_init(void)
{
int retval;
printk(KERN_INFO "chrdev_init helloworld init\n");
/*
// 在module_init宏调用的函数中去注册字符设备驱动
// major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号
// 内核如果成功分配就会返回分配的主设备号;如果分配失败会返回负数
mymajor = register_chrdev(0, MYNAME, &test_fops);
if (mymajor < 0)
{
printk(KERN_ERR "register_chrdev fail\n");
return -EINVAL;
}
printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);
*/
// 使用新的cdev接口来注册字符设备驱动
// 新的接口注册字符设备驱动需要2步
// 第1步:注册/分配主次设备号
// mydev = MKDEV(MYMAJOR, 0);
// retval = register_chrdev_region(mydev, MYCNT, MYNAME);
retval = alloc_chrdev_region(&mydev, 12, MYCNT, MYNAME);
if (retval) {
// printk(KERN_ERR "Unable to register minors for %s\n", MYNAME);
// return -EINVAL;
printk(KERN_ERR "Unable to alloc minors for %s\n", MYNAME);
goto flag1;
}
// printk(KERN_INFO "register_chrdev_region success\n");
printk(KERN_INFO "alloc_chrdev_region success\n");
printk(KERN_INFO "major = %d, minor = %d.\n", MAJOR(mydev), MINOR(mydev));
// 第2步:注册字符设备驱动
// 给pcdev分配内存,指针实例化
pcdev = cdev_alloc();
// cdev_init(&test_cdev, &test_fops);
// cdev_init(pcdev, &test_fops);
pcdev->owner = THIS_MODULE;
pcdev->ops = &test_fops;
// retval = cdev_add(&test_cdev, mydev, MYCNT);
retval = cdev_add(pcdev, mydev, MYCNT);
if (retval) {
printk(KERN_ERR "Unable to cdev_add\n");
goto flag2;
}
printk(KERN_INFO "cdev_add success\n");
// 注册字符设备驱动完成后,添加设备类的操作,以让内核帮我们发信息
// 给udev,让udev自动创建和删除设备文件
test_class = class_create(THIS_MODULE, "aston_class");
if (IS_ERR(test_class))
return -EINVAL;
// 最后1个参数字符串,就是我们将来要在/dev目录下创建的设备文件的名字
// 所以我们这里要的文件名是/dev/test
device_create(test_class, NULL, mydev, NULL, "test_chrdev");
/*
// 使用动态映射的方式来操作寄存器
if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
// return -EINVAL;
goto flag3;
if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0DAT"))
// return -EINVAL;
goto flag3;
pGPJ0CON = ioremap(GPJ0CON_PA, 4);
pGPJ0DAT = ioremap(GPJ0DAT_PA, 4);
*pGPJ0CON = 0x11111111;
*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
*/
/*
// 使用结构体和动态映射的方式来操作寄存器
if (!request_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t), "GPJ0REG"))
return -EINVAL;
pGPJ0REG = ioremap(GPJ0_REGBASE, sizeof(gpj0_reg_t));
// 映射之后用指向结构体的指针来进行操作
// 指针使用->结构体内元素的方式来操作各个寄存器
pGPJ0REG->gpj0con = 0x11111111;
pGPJ0REG->gpj0dat = ((0<<3) | (0<<4) | (0<<5));
*/
// 使用动态映射的方式来操作寄存器
if (!request_mem_region(GPJ0_REGBASE, 8, "GPJ0REG"))
goto flag3;
baseaddr = ioremap(GPJ0_REGBASE, 8);
writel(0x11111111, baseaddr + S5P_GPJ0CON);
writel(((0<<3) | (0<<4) | (0<<5)), baseaddr + S5P_GPJ0DAT);
/*
// 模块安装命令insmod时执行的硬件操作
rGPJ0CON = 0x11111111;
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
printk(KERN_INFO "GPJ0CON = %p.\n", GPJ0CON);
printk(KERN_INFO "GPJ0DAT = %p.\n", GPJ0DAT);
*/
// goto flag0:
return 0;
// 如果第4步才出错跳转到这里来
flag4:
// release_mem_region(GPJ0CON_PA, 4);
// release_mem_region(GPJ0DAT_PA, 4);
// release_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t));
release_mem_region(GPJ0_REGBASE, 8);
// 如果第3步才出错跳转到这里来
flag3:
// cdev_del(&test_cdev);
cdev_del(pcdev);
// 如果第2步才出错跳转到这里来
flag2:
// 在这里把第1步做成功的东西给注销掉
unregister_chrdev_region(mydev, MYCNT);
// 如果第1步才出错跳转到这里来
flag1:
return -EINVAL;
// flag0:
// return 0;
}
// 模块卸载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit helloworld exit\n");
// rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
// *pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
// pGPJ0REG->gpj0dat = ((1<<3) | (1<<4) | (1<<5));
writel(((1<<3) | (1<<4) | (1<<5)), baseaddr + S5P_GPJ0DAT);
/*
// 解除映射
iounmap(pGPJ0CON);
iounmap(pGPJ0DAT);
release_mem_region(GPJ0CON_PA, 4);
release_mem_region(GPJ0DAT_PA, 4);
*/
/*
// 解除映射
// iounmap(pGPJ0REG);
// release_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t));
*/
iounmap(baseaddr);
release_mem_region(GPJ0_REGBASE, 8);
device_destroy(test_class, mydev);
class_destroy(test_class);
/*
// 在module_exit宏调用的函数中去注销字符设备驱动
unregister_chrdev(mymajor, MYNAME);
*/
// 使用新的接口来注销字符设备驱动
// 注销分2步:
// 第一步真正注销字符设备驱动用cdev_del
// cdev_del(&test_cdev);
cdev_del(pcdev);
// 第二步去注销申请的主次设备号
unregister_chrdev_region(mydev, MYCNT);
}
module_init(chrdev_init);
module_exit(chrdev_exit);
// MODULE_xxx这种宏作用是用来添加模块描述信息
// 描述模块的许可证
MODULE_LICENSE("GPL");
// 描述模块的作者
MODULE_AUTHOR("aston");
// 描述模块的介绍信息
MODULE_DESCRIPTION("module test");
// 描述模块的别名信息
MODULE_ALIAS("alias xxx");