个人笔记向
上一次完成了一个驱动程序的框架,这次我来实现点灯。
首先呢,创建一个led.c文件用来初始化驱动程序。
创建好后第一件事就是驱动的注册与卸载,以及个人信息标注和添加模块 LICENSE 信息。
//注册与卸载
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("lakzhu");
紧接着的就是 led_init、led_exit 这两个函数的编写了:
#define LED_MAJOR 200
#define LED_NAME "led"
const const struct file_operations led_fops = {
.owner = THIS_MODULE,
.write = led_write,
.open = led_open,
.release = led_release,
};
static int __init led_init(void)
{
ret = register_chrdev(LED_MAJOR,LED_NAME,&led_fops);
if(ret < 0){
printk("register error\r\n");
return -EIO;
}
printk("init\r\n");
return 0;
}
static void __exit led_exit(void)
{
unregister_chrdev(LED_MAJOR,LED_NAME);
printk("exit\r\n");
}
在 led_init 里面要进行 led 驱动的设备号、设备名和设备结构体构建,使用 register_chrdev() 函数来注册,在这个 led_fops 设备结构体中写好调用的函数。.owner 一般就设置成 THIS_MODULE;write 函数用于向设备文件写入(发送)数据;open 函数用于打开设备文件;release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应;
在 led_exit 里面要把申请过的设备号资源等进行卸载,使用 unregister_chrdev() 函数即可。
接着写好 led_fops 结构体中的函数:
static int led_open(struct inode *inode, struct file *fp)
{
printk("led_open\r\n");
return 0;
}
static int led_release(struct inode *inode, struct file *fp)
{
printk("led_release\r\n");
return 0;
}
static ssize_t led_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
unsigned int retval;
unsigned char databuff[1];
retval = copy_from_user(databuff, buf, count);
if(retval < 0){
printk("write error");
return -EFAULT;
}
switch_led(databuff[0]);
return 0;
}
led_write 函数尤为重要,使用 copy_from_user函数传到驱动里面的数据复制到驱动里面。第一个值是存储数据的变量,第二个是需要传入的数据,第三个是传输总数。然后根据写入的数据来开关led。
led 的使用,我们这里使用寄存器来控制 led 的亮灭。
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)
//地址映射后的指针
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;
#define LEDOFF 0
#define LEDON 1
void switch_led(unsigned char temp){
unsigned char val = 0;
if(temp == 1){
val = readl(GPIO1_DR);
val &= ~(1 << 3);
writel(val,GPIO1_DR);
}
else{
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val,GPIO1_DR);
}
}
static int __init led_init(void)
{
int ret = 0;
unsigned int val = 0;
//初始化 地址映射 获取虚拟地址
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE,4);
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE,4);
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE,4);
GPIO1_DR = ioremap(GPIO1_DR_BASE,4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE,4);
//时钟初始化
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26);
val |= 3 << 26;
writel(val,IMX6U_CCM_CCGR1);
writel(0x5,SW_MUX_GPIO1_IO03);
writel(0X10B0,SW_PAD_GPIO1_IO03);
val = readl(GPIO1_GDIR);
val |= 1 << 3;
writel(val,GPIO1_GDIR);
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val,GPIO1_DR);
//注册字符设备
ret = register_chrdev(LED_MAJOR,LED_NAME,&led_fops);
if(ret < 0){
printk("register error\r\n");
return -EIO;
}
printk("init\r\n");
return 0;
}
static void __exit led_exit(void)
{
// unsigned int val = 0;
// val = readl(GPIO1_DR);
// val |= (1 << 3);
// writel(val,GPIO1_DR);
//取消地址映射
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
unregister_chrdev(LED_MAJOR,LED_NAME);
printk("exit\r\n");
}
地址映射
我们先简单了解一下 MMU 这个神器,MMU 全称叫做 Memory
Manage Unit,也就是内存管理单元。在老版本的 Linux 中要求处理器必须有 MMU,但是现在
Linux 内核已经支持无 MMU 的处理器了。MMU 主要完成的功能如下:
①、完成虚拟空间到物理空间的映射。
②、内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。
我们重点来看一下第①点,也就是虚拟空间到物理空间的映射,也叫做地址映射。首先了
解两个地址概念:虚拟地址(VA,Virtual Address)、物理地址(PA,PhyscicalAddress)。对于 32 位
的处理器来说,虚拟地址范围是 2^32=4GB,我们的开发板上有 512MB 的 DDR3,这 512MB 的
内存就是物理内存,经过 MMU 可以将其映射到整个 4GB 的虚拟空间。
Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚
拟地址。比如 I.MX6ULL 的 GPIO1_IO03 引脚的复用寄存器
IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 的地址为 0X020E0068。如果没有开启 MMU 的话
直接向 0X020E0068 这个寄存器地址写入数据就可以配置 GPIO1_IO03 的复用功能。现在开启
了 MMU,并且设置了内存映射,因此就不能直接向 0X020E0068 这个地址写入数据了。我们必
须得到 0X020E0068 这个物理地址在 Linux 系统里面对应的虚拟地址,这里就涉及到了物理内
存和虚拟内存之间的转换,需要用到两个函数:ioremap 和 iounmap
ioremap 函数用于获取指 定 物 理 地 址 空 间 对 应 的 虚 拟 地 址 空 间
#define ioremap(cookie,size) __arm_ioremap((cookie), (size),
MT_DEVICE)
void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size,
unsigned int mtype)
{
return arch_ioremap_caller(phys_addr, size, mtype,
__builtin_return_address(0));
}
ioremap 是个宏,有两个参数:cookie 和 size,真正起作用的是函数__arm_ioremap,此函
数有三个参数和一个返回值,这些参数和返回值的含义如下:
phys_addr:要映射的物理起始地址。
size:要映射的内存空间大小。
mtype:ioremap 的类型,可以选择 MT_DEVICE、MT_DEVICE_NONSHARED、
MT_DEVICE_CACHED 和 MT_DEVICE_WC,ioremap 函数选择 MT_DEVICE。
返回值:__iomem 类型的指针,指向映射后的虚拟空间首地址
iounmap 函数
卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射,iounmap 函数原
型如下
void iounmap (volatile void __iomem *addr)
iounmap 只有一个参数 addr,此参数就是要取消映射的虚拟地址空间首地址.
通过 ioremap 函数把 led 寄存器的物理地址就能自动转化为虚拟地址返回。这样就能够使用 readl 和 writel 函数来控制 led 了。
ledApp 函数编写:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define LEDOFF 0
#define LEDON 1
int main(int argc,char *argv[])
{
int fd;
int ret;
char *filename;
unsigned char databuff[1];
if(argc != 3){
printf("data deficiency\r\n");
}
filename = argv[1];
fd = open(filename, O_RDWR);
if(fd < 0){
printf("file open errer\r\n");
return -1;
}
databuff[0] = atoi(argv[2]);
ret = write(fd,databuff,sizeof(databuff));
if(ret < 0){
printf("control failed\r\n");
close(fd);
return -1;
}
close(fd);
return 0;
}