前言
原先我们使用了的linux内核GPIO资源方式来编写LED控制的字符驱动,这次我们使用ioremap形式来操作从而实现像裸机开发中直接操作寄存器方式来操作外设。
工程代码:
链接:https://pan.baidu.com/s/1AnLXtcpuVI336peqw-ArSg
提取码:ur15
建议:
这里的字符驱动因为是固定的一种代码编程框架,所以直接使用代码即可。根据需求只需要修改需要修改的地方,其他地方保持不变就行。
硬件信息:
1. 查看开发板上LED对应的端口
打开开发板底板的电路原理图,搜索找到LED记录它的网络标号
然后根据网络标号搜索找到主板上处理器对应端口号
所以这两个LED灯对应的端口就是:GPL2_0、GPK1_1
查阅芯片Exynos4412用户手册
打到GPIO章节的GPK1相关寄存器,这里只需要两个寄存器(控制寄存器、数据寄存器),这里需要用到它们的地址和它们具体位描述。代码编写需要参考这些
记录GPL2_0、GPK1_1两个端口对应的寄存器(控制寄存器、数据寄存器)的地址。同时从数据手册中可以看到外设寄存器大部都是一段连续的内存地址而且占用大小为4字节。这样我们就可以使用结构体将相关的寄存器统一起来,这个操作在单片机的库函数中有大量的应用。
GPK1_1相关寄存器:
寄存器名称 | 地址 |
GPK1CON | 0x1100_0060 |
GPK1DAT | 0x1100_0064 |
GPK1PUD | 0x1100_0068 |
GPL2_0相关寄存器:
寄存器名称 | 地址 |
GPL2CON | 0x1100_0100 |
GPL2DAT | 0x1100_0104 |
GPL2PUD | 0x1100_0108 |
虚拟地址与物理地址对应
使用ioremap函数可以将物理地址转换成Linux内核用到的虚拟地址,然后我们就可以像裸机编程一样直接操作寄存器进而控制完成相关外设的控制。
虚拟物理地址映射资源申请:ioremap
虚拟物理地址映射资源释放:iounmap
代码逻辑:
驱动代码:实现ioctl原型,在这个函数内部通过用户传递过来的参数来选择并控制led灯状态
完整代码:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/device.h>
/*Linux中申请GPIO的头文件*/
#include <linux/gpio.h>
/*三星平台的GPIO配置函数头文件*/
/*三星平台EXYNOS系列平台,GPIO配置参数宏定义头文件*/
#include <plat/gpio-cfg.h>
/*三星平台4412平台,GPIO宏定义头文件*/
#include <mach/gpio-exynos4.h>
#include <asm/io.h>
#include <asm/uaccess.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Jack.Tang");
/**
* 说明:字符设备名称、字符设备次设备数量、次设备编号值
* 修改:根据具体需求进行修改
**/
#define DEVICE_NAME "my_led_device"
#define DEVICE_MINOR_NUM 1 //字符设备次设备数量
#define DEVICE_MINOR_VALUE 0 //次设备编号值
#define BUFFER_SIZE 2048 //申请2K内存
/**
* 说明:定时器结构体
**/
struct gpio_l2
{
unsigned int GPL2CON;
unsigned int GPL2DAT;
unsigned int GPL2PUD;
} * GPIO_L2; //定义GPIO_L2结构体指针变量
struct gpio_k1
{
unsigned int GPK1CON;
unsigned int GPK1DAT;
unsigned int GPK1PUD;
} * GPIO_K1; //定义GPIO_K1控制结构体指针变量
/**
* 说明:寄存器的物理基地址
* 修改:根据具体外设的基地址来设置,这个需要查看芯片的数据手册
* 注意:宏定义后面没有";"号
**/
#define phy_addr_gpio_k1 (0x11000060) //GPK1CON基地址
#define phy_addr_gpio_l2 (0x11000100) //GPL2CON基地址
volatile unsigned int vir_addr_gpio_k1;
volatile unsigned int vir_addr_gpio_l2;
int char_dev_major = 0; //主设备号--设置0表示让系统自动分配
int char_dev_minor = DEVICE_MINOR_VALUE; //次设备号
struct reg_char_device
{
char *data;
unsigned long size;
struct cdev chardev;
};
/**
* 说明:LED灯枚举;
**/
enum LEDS
{
LED2, //--GPL2_0
LED3, //--GPK1_1
ALL //--全部LED灯
};
/**
*功能:led灯关闭程序;
*参数:led_num:表示LED的枚举;
**/
void led_off(enum LEDS led_num)
{
switch (led_num)
{
case LED2: // LED2灯--GPL2_0
GPIO_L2->GPL2DAT &= 0xfe; //清除最低位值
break;
case LED3: // LED3灯--GPK1_1
GPIO_K1->GPK1DAT &= 0xfd; //清除低二位位为0
break;
default:
case ALL: // 所有LED灯
//关闭LED
GPIO_L2->GPL2DAT &= 0xfe; //清除最低位值
GPIO_K1->GPK1DAT &= 0xfd; //清除低二位位为0
break;
}
}
/**
*功能:led灯打开程序;
*参数:led_num:表示LED的枚举;
**/
void led_on(enum LEDS led_num)
{
switch (led_num)
{
case LED2: // LED2灯--GPL2_0
GPIO_L2->GPL2DAT |= 0x01; //置最低位为1
break;
case LED3: // LED3灯--GPK1_1
GPIO_K1->GPK1DAT |= 0x02; //置低第二位位为1
break;
default:
case ALL: // 所有LED灯
//点亮所有LED
GPIO_L2->GPL2DAT |= 0x01; //清除最低位值
GPIO_K1->GPK1DAT |= 0x02; //清除最低位值
break;
}
}
//-----------------------------定义函数----------------------------------
static int my_char_dev_open(struct inode *node, struct file *file)
{
printk(KERN_EMERG "my_char_dev is open\n");
return 0;
}
static int my_char_dev_release(struct inode *node, struct file *file)
{
printk(KERN_EMERG "my_char_dev is release\n");
return 0;
}
/**
* 说明:io控制
* 修改:根据具体需求来修改
**/
static long my_char_dev_ioctl(struct file *file, unsigned int cmd, unsigned long args)
{
if (cmd == 1)
{
if (args >= 2) //参数值>=2
{
printk(KERN_EMERG "args is error!\n");
return -1;
}
led_on((enum LEDS)args); //打开LED灯
printk(KERN_EMERG "turn on the led_%ld\n", args);
}
else if (cmd == 0)
{
if (args >= 2) //参数值>=2
{
printk(KERN_EMERG "args is error!\n");
return -1;
}
led_off((enum LEDS)args); //关闭LED灯
printk(KERN_EMERG "turn off the led_%ld\n", args);
}
printk(KERN_EMERG "cmd is %d,args is %ld\n", cmd, args);
return 0;
}
static ssize_t my_char_dev_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{
printk(KERN_EMERG "my_char_dev is read\n");
return 0;
}
static ssize_t my_char_dev_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
printk(KERN_EMERG "my_char_dev is read\n");
return 0;
}
//文件结构体
struct file_operations opts = {
.owner = THIS_MODULE,
.open = my_char_dev_open,
.release = my_char_dev_release,
.read = my_char_dev_read,
.write = my_char_dev_write,
.unlocked_ioctl = my_char_dev_ioctl,
};
struct reg_char_device *arr_char_dev = NULL; //定义reg_char_device结构体指针
struct class *char_dev_class = NULL; //定义类结构体
/**
* 功能:初始化控制LED的GPIO
**/
void led_gpio_Init(void)
{
/**
* 说明:将物理地址与虚拟地址进行映射
**/
vir_addr_gpio_k1 = (volatile unsigned int)ioremap(phy_addr_gpio_k1, 0x10); //0x10表示分配的内存大小:16字节
GPIO_K1 = (struct gpio_k1 *)vir_addr_gpio_k1;
vir_addr_gpio_l2 = (volatile unsigned int)ioremap(phy_addr_gpio_l2, 0x10); //0x10表示分配的内存大小:16字节
GPIO_L2 = (struct gpio_l2 *)vir_addr_gpio_l2;
/**
* 说明:设置GPIO模式、关闭LED(置高电平)、上拉
**/
GPIO_L2->GPL2CON |= 0x00000001; //设置GPL2_0为输出模式
GPIO_K1->GPK1CON |= 0x00000010; //设置GPK1_1为输出模式
//配置GPIO为上拉
GPIO_L2->GPL2PUD |= 0x0003; //设置GPL2_0上拉
GPIO_K1->GPK1PUD |= 0x000c; //设置GPK1_1上拉
//关闭LED
GPIO_L2->GPL2DAT &= 0xfe; // GPL2_0清0
GPIO_K1->GPK1DAT &= 0xfd; // GPK1_1清0
}
/**
*初始化注册设备号
**/
static void register_char_dev(struct reg_char_device *pchar_dev, int minordev_num)
{
int i = 0, err = 0;
for (i = 0; i < minordev_num; i++)
{
((pchar_dev + i)->chardev).owner = THIS_MODULE;
((pchar_dev + i)->chardev).ops = &opts; //给cdev字符设备文件结构体填充数据
cdev_init(&((pchar_dev + i)->chardev), &opts); //字符类设备初始化
//向系统注册设备
err = cdev_add(&((pchar_dev + i)->chardev), MKDEV(char_dev_major, char_dev_minor + i), minordev_num);
if (err < 0)
{
printk(KERN_EMERG "cdev_init is failed!\n");
return;
}
printk(KERN_EMERG "char device minor %d add success!\n", char_dev_minor + i);
}
}
static int ioremap_led_init(void)
{
int i = 0;
int ret = 0;
dev_t chardev; //定义设备号变量
ret = alloc_chrdev_region(&chardev, char_dev_minor, DEVICE_MINOR_NUM, DEVICE_NAME); //动态申请设备号
if (ret < 0) //动态注册出错
{
printk(KERN_EMERG "alloc_chrdev_region failed!\n");
return -1;
}
char_dev_major = MAJOR(chardev); //获取主设备号
printk(KERN_EMERG "char device major is %d\n", char_dev_major);
char_dev_class = class_create(THIS_MODULE, DEVICE_NAME); //创建class类
//给定义的字符结构体申请内存
arr_char_dev = kmalloc(DEVICE_MINOR_NUM * sizeof(struct reg_char_device), GFP_KERNEL);
if (arr_char_dev == NULL) //内存申请失败
{
printk(KERN_EMERG "kmalloc memory to arr_char_dev is failed!\n");
return -1;
}
for (i = 0; i < DEVICE_MINOR_NUM; i++) //给buffer缓存申请内存空间
{
(arr_char_dev + i)->data = kmalloc(BUFFER_SIZE, GFP_KERNEL);
if ((arr_char_dev + i)->data == NULL) //申请buffer缓存内存空间失败
{
printk(KERN_EMERG "kmalloc memory to arr_char_dev[%d].data is failed!\n", i);
return -1;
}
//创建字符类设备
device_create(char_dev_class, NULL, MKDEV(char_dev_major, char_dev_minor + i), NULL, DEVICE_NAME "%d", i);
}
//初始化设备
register_char_dev(arr_char_dev, DEVICE_MINOR_NUM);
//初始化LED的GPIO
led_gpio_Init();
printk(KERN_EMERG "char device initialization...\n");
return 0;
}
static void ioremap_led_exit(void)
{
int i = 0;
for (i = 0; i < DEVICE_MINOR_NUM; i++)
{
kfree((arr_char_dev + i)->data); //释放buffer缓存内存
device_destroy(char_dev_class, MKDEV(char_dev_major, char_dev_minor + i)); //摧毁设备节点
cdev_del(&((arr_char_dev + i)->chardev)); //卸载设备
}
kfree(arr_char_dev); //释放内存
iounmap((void volatile __iomem *)vir_addr_gpio_k1);
iounmap((void volatile __iomem *)vir_addr_gpio_l2);
class_destroy(char_dev_class); //删除设备类
unregister_chrdev_region(MKDEV(char_dev_major, char_dev_minor + i), DEVICE_MINOR_NUM); //删除设备号
printk(KERN_EMERG "char device exit...\n");
}
module_init(ioremap_led_init);
module_exit(ioremap_led_exit);
Makefile文件:
#!/bin/bash
obj-m += charDriver_led.o
#这个是linux内核路径需要根据自己的linux内核路径来设置
KDIR := /home/jack/my_work/iTop4412_Kernel_3.0
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
rm -rf *.o *.ko .mod.o *.mod.c *.symvers *.order
应用程序代码
代码逻辑:通过file文件操作来打开驱动代码生成的字符设备节点,然后从键盘输入控制指令并将指令通过ioctl来控制led状态。
完整代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#define CMD_SIZE 20
#define CMD_NUM 2
#define PWM_VALUE "pwm_value"
#define PWM_PLUS "pwm_plus"
int main(int argc, char **argv)
{
int fd;
// cmd:控制LED开头;agrs: 选择哪个LED灯
int led_cmd, led_num;
/**
* 说明:字符设备节点名称
* 修改:根据字符驱动中具体的设备节点名称修改
**/
const char *led_file = "/dev/my_led_device0";
fd = open(led_file, O_RDWR | O_NDELAY);
if (fd < 0)
{
printf("APP open %s failed!\n", led_file);
return -1;
}
printf("please input char command: the first agument is control led, the second is select led\n");
printf("first argument:'1' means turn on the led,'0' means turn off the led\n");
printf("second argument: '0': select LED2,'1': select LED2.'2': select all leds\n");
printf("exit: input '-1'\n");
while (1)
{
scanf("%d,%d", &led_cmd, &led_num); //输入指令
if ((led_cmd == -1) || (led_num == -1)) //
{
break;
}
ioctl(fd, led_cmd, led_num);
}
close(fd); //关闭文件
printf("close the fd success!\n");
return 0;
}
编译命令:arm-none-linux-gnueabi-gcc -o led_test led_test.c -static
整个程序运行测试
通过网络将开发板与linux主机ubuntu连接上,然后挂载nfs文件服务。这样我们在开发板上就可以直接操作linux主机上的文件了。具体的nfs文件操作步骤,请自行百度。网上类似文章也多,同时这个nfs挂载操作部分比较多,放在这里不合适。
开发板上挂载nfs服务:
将在开发板的/mnt目录下看到linux主机/nfsroot中的内容。
[root@iTOP-4412]# mount -t nfs 192.168.1.110:/home/jack/nfs /mnt/ -o nolock
加载led字符驱动:
[root@iTOP-4412]# insmod charDriver_led.ko
执行led应用程序:
[root@iTOP-4412]# ./app_ioremap_led
测试结果:
[ 734.013650] my_char_dev is open
please input char command: the first agument is control led, the second is select led
first argument:'1' means turn on the led,'0' means turn off the led
second argument: '0': select LED2,'1': select LED2.'2': select all leds
0,0
[ 741.855869] turn off the led_0
[ 741.857540] cmd is 0,args is 0
1,0
[ 750.112384] turn on the led_0
[ 750.113972] cmd is 1,args is 0
0,2
[ 763.945486] turn off the led_2
[ 763.947095] cmd is 0,args is 2
同时我们可以看到iTOP4412开发板上的LED灯会根据我们输入的命令参数进行相应的动作。