Linux驱动:Tiny4412开发板上点亮LED灯程序(GPIO驱动)

上篇博文以globalmem为例实现了一个虚拟的字符设备驱动,本文将在上文的基础上,以点亮LED实例来介绍GPIO字符设备驱动,将不重复上篇相同内容。

环境:主机-Ubuntu 16.04,开发板-友善之臂tiny4412开发板,内核版本linux-3.5,参考tiny4412相关手册。

板上硬件资源:

注:实践发现,本开发板实际使用的分别是:GPM4_0、GPM4_1、GPM4_2、GPM4_3(可能硬件版本不一)

重点意外:Tiny4412自带内核已把LED驱动编进了内核,因此,需重新配置内核将其取消或编译成模块,再重新烧写内核。

 

一、GPIO相关寄存器简介

Tiny4412采用的是Samsung ARM Cortex-A9 四核 Exynos 4412 Quad-core处理器,运行主频1.5G。。。

CPU寄存器相关的关键是看用户手册(User's Manual)

《Exynos 4412 SCP_Users Manual_Ver.0.10.00_Preliminary0.pdf》

GPIO相关的章节见 --- “ 6 General Purpose Input/Ouput (GPIO) Control

看到 6.2 Register Description ,6.2.1是总的概述,6.2.2~5是具体说明。

GPIO相关寄存器主要有以下几种:

寄存器描述备注
GPxxCONconfiguration register配置寄存器,配置输入/输出/IO复用等功能
GPxxDATdata register数据寄存器,读取输入数据/设置输出数据
GPxxPUDpull-up/down register上/下拉寄存器,配置上下拉状态
GPxxDRVdrive strength control register驱动强度控制寄存器,配置IO驱动能力
GPxxCONPDNpower down mode configuration register掉电模式配置寄存器,配置输入/输出
GPxxPUDPDNpower down mode pull-up/down register掉电模式上下拉寄存器,配置上下拉状态

还有关键的,寄存器地址及其详细描述,如下:

1、配置寄存器GPxxCON:

其地址Address就是:Base Address(基地址)+offset(偏移),即 0x1100 0000 + 0x02E0;

一个寄存器是4个字节32bit大小,按位定义每4bit一组共8组分别对应GPM4_0~7等8个IO。(上图不完整)

再看右边的数值定义,0x0 = Input, 0x1=Output ...等已说明很具体了,只不过是给对应的位赋值这么简单。

2、数据寄存器GPxxDAT:

同理,[0~7]位分别对GPM4_0~7,还说:当配置成输入时,读取对应位的值就是对应IO的状态了;配置成输出时,写入对应位的值就是对应IO的状态了;当配置成功能引脚时如(UART),其值是未定义的。

3、上下拉寄存器GPxxPUD:

2n+1:2n即2位对应一个IO,根据Description的定义来赋值。

。。。其他寄存器就不一一讲了,大同小异,看手册就可以了。

操作某个IO(UART、I2C、SPI等所有片上外设),最终目的都是通过给这些寄存器赋值使其工作起来。

 

二、寄存器操作

直接操作寄存器是一种简单粗暴的方法:

简单---只需知道寄存器地址、各bit的作用就可以了;

粗暴---直接进行地址操作,直接读写地址的值。

以32位CPU为例,读写操作如下:???

/* 读32位寄存器的值 */
unsigned int reg32_read(unsigned int addr)
{
	return *(volatile unsigned int *)addr;
}

/* 写32位寄存器的值 */
void reg32_write(unsigned int addr, unsigned int data)
{
	*(volatile unsigned int *)addr = data;
}

当然,上述几行代码虽简单,在裸奔的单片机或实时系统等方案上可行,

但是,linux内核不允许直接操作物理地址PA,只能操作虚拟地址VA,其提供一套机制来进行映射转换:

/*    io内存映射,将物理地址映射为虚拟地址
 *    cookie --- physical addr 物理地址
 *    size --- 映射大小
 * 
 *    return --- virtual addr 映射后的虚拟地址
 */
#define ioremap(cookie,size)		__arm_ioremap((cookie), (size), MT_DEVICE)


/*    取消映射
 *    addr--- ioremap得到的地址
 * 
 */
void iounmap(void *addr)


/* 读出io内存映射地址c中的32位值 */
#define readl(c)		({ u32 __v = readl_relaxed(c); __iormb(); __v; })

/* 向io内存映射地址c中写入32位值v */
#define writel(v,c)		({ __iowmb(); writel_relaxed(v,c); })

因此,linux要操作寄存器需作IO内存映射处理,再在映射的内存地址上进行操作;

说白了就是,驱动也不能直接访问物理地址PA,要先将PA映射成VA,再在VA上操作,也能达到访问PA寄存器的效果。

 

GPIO如何操作?以点亮LED为例:

0、ioremap()将IO进行内存映射,得到可操作的虚拟内存(地址);

1、在映射地址中,设置控制寄存器(GPXX_CON):Output模式;

2、在映射地址中,设置上下拉寄存器(GPXX_PUD):不上下拉;

3、设置数据寄存器(GPXX_DAT):往对应位写0或1;

注意:写值时,应先读出某个寄存器的值,再对相应位进行操作,其他位应保留原值。

详情见以下代码!

 

三、GPIO相关操作函数简介

以GPIO函数的方式,则需用到以下函数:

/* 申请IO资源 */
int gpio_request(unsigned gpio, const char *label)

/* 释放IO资源 */
void gpio_free(unsigned gpio)

/* 配置IO功能/模式---配置CON寄存器 */
int s3c_gpio_cfgpin(unsigned int pin, unsigned int config)

/* 配置IO上下拉模式---配置PUD寄存器 */
int s3c_gpio_setpull(unsigned int pin, samsung_gpio_pull_t pull)

/* 设置IO的输出值---配置DAT寄存器 */
void gpio_set_value(unsigned int gpio, int value)

可见,除申请IO资源外,其他GPIO函数的最终目的---也是设置相应的寄存器,流程与操作寄存器方式类似。

具体应用见以下代码!

 

四、自动创建设备类及设备节点

如何在字符驱动中自动创建设备节点,而不需手动通过命令mknod来创建呢?

由如下函数实现:

/* 创建设备类 */
/* This is a #define to keep the compiler from merging different
 * instances of the __key variable */
#define class_create(owner, name)		\
({						\
	static struct lock_class_key __key;	\
	__class_create(owner, name, &__key);	\
})

/* 销毁设备类 */
void class_destroy(struct class *cls)


/* 创建设备并注册到sysfs */
struct device *device_create(struct class *class, struct device *parent,
			     dev_t devt, void *drvdata, const char *fmt, ...)

/* 销毁设备 */
void device_destroy(struct class *class, dev_t devt)


内核中定义了struct class结构体,顾名思义,一个struct class结构体类型变量对应一个类,内核同时提供了class_create(…)函数,可以用它来创建一个类,这个类存放于sysfs下面,一旦创建好了这个类,再调用device_create(…)函数来在/dev目录下创建相应的设备节点。这样,加载模块的时候,用户空间中的udev会自动响应device_create(…)函数,去/sysfs下寻找对应的类从而创建设备节点。
 

五、源码

首先,驱动模型还是那一套(上篇有介绍),驱动加载时创建设备类及设备(init),打开设备时申请资源及配置功能(open),控制设备时根据命令设备引脚电平(ioctl),。。。

本例程实现了两种驱动方式 --- IO映射方式、GPIO函数方式,通过宏 IOMAP_REG_ACCESS 控制。

1、GPIO驱动源码:

gpio_led_drv.c:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/gpio.h>
#include <plat/gpio-cfg.h>
#include <linux/io.h>

//#define IOMAP_REG_ACCESS		// 操作IO寄存器方式实现

/* 控制命令 */
enum {
	LED_ALL_ON,
	LED_ALL_OFF,
};

#define LED_VAL_ON		0
#define LED_VAL_OFF		1

static int led_gpios[] = {
	EXYNOS4X12_GPM4(0),
	EXYNOS4X12_GPM4(1),
	EXYNOS4X12_GPM4(2),
	EXYNOS4X12_GPM4(3),
};

#define LED_COUNT		ARRAY_SIZE(led_gpios)

/* 实例化led */
dev_t devon;
struct cdev led_cdev;
struct class *dev_class = NULL;
struct device *dev_led = NULL;
void *va_base_p2 = NULL;

#define CON_MODE_OUTPUT		0x1				// output mode
#define PA_BASE_ADDR_P2		0x11000000		// physical address
#define OFFSET_GPM4_CON		0x02E0			// control reg
#define OFFSET_GPM4_DAT		0x02E4			// data reg

ssize_t gpio_led_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
	printk("%s -------- enter ...\n", __FUNCTION__);
	return 0;
}

ssize_t gpio_led_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)
{
	printk("%s -------- enter ...\n", __FUNCTION__);
	return 0;
}

/* 控制命令处理函数 */
long gpio_led_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	int i;

	switch(cmd)
	{
		case LED_ALL_ON:
			for(i=0; i<LED_COUNT; i++)
			{
			#ifdef IOMAP_REG_ACCESS
				writel(0x00, va_base_p2+OFFSET_GPM4_DAT);
			#else
				gpio_set_value(led_gpios[i], LED_VAL_ON);
			#endif
			}
			break;

		case LED_ALL_OFF:
			for(i=0; i<LED_COUNT; i++)
			{
			#ifdef IOMAP_REG_ACCESS
				writel(0xFF, va_base_p2+OFFSET_GPM4_DAT);
			#else
				gpio_set_value(led_gpios[i], LED_VAL_OFF);
			#endif
			}
			break;

		default:
			break;
	}

	return 0;
}

int gpio_led_open(struct inode *inode, struct file *filp)
{
	char label_name[] = "led_M4_x";
	int reg_val;
	int i, ret;
	
	printk("%s -------- enter ...\n", __FUNCTION__);

#ifdef IOMAP_REG_ACCESS
	/* 映射寄存器IO地址空间 */
	va_base_p2 = ioremap(PA_BASE_ADDR_P2, 0x02FF);

	/* 先读出配置寄存器值-再修改(其他位保留原值) */
	reg_val = readl(va_base_p2 +OFFSET_GPM4_CON);
	for(i=0; i<LED_COUNT; i++)
	{
		/* 将reg对应4位设置为output模式 */
		reg_val = (reg_val&(~(0xF<<(i*4)))) | (CON_MODE_OUTPUT<<(i*4));
	}
	writel(reg_val, va_base_p2 +OFFSET_GPM4_CON);
#else
	/* 申请GPIO资源 */
	for(i=0; i<LED_COUNT; i++)
	{
		label_name[strlen(label_name)-1] = '0'+i;
		ret = gpio_request(led_gpios[i], label_name);
		if(ret < 0)
			goto ERR_GPIO_REQ;
		
		s3c_gpio_cfgpin(led_gpios[i], S3C_GPIO_OUTPUT);
		s3c_gpio_setpull(led_gpios[i], S3C_GPIO_PULL_NONE);
		gpio_set_value(led_gpios[i], LED_VAL_OFF);
	}
#endif
	return 0;
	
#ifndef IOMAP_REG_ACCESS
	ERR_GPIO_REQ:
	for(i-=1; i>=0; i--)
	{
		gpio_free(led_gpios[i]);
	}

	return -1;
#endif
}

int gpio_led_release(struct inode *inode, struct file *filp)
{
	int i;

	printk("%s -------- enter ...\n", __FUNCTION__);

#ifdef IOMAP_REG_ACCESS
	/* 取消内存映射 */
	iounmap(va_base_p2);
#else
	/* 释放GPIO资源 */
	for(i=0; i<LED_COUNT; i++)
		gpio_free(led_gpios[i]);
#endif
	return 0;
}

/* 文件操作结构体 */
static const struct file_operations gpio_led_fops = 
{
	.owner = THIS_MODULE,
	.read = gpio_led_read,
	.write = gpio_led_write,
	.unlocked_ioctl = gpio_led_ioctl,
	.open = gpio_led_open,
	.release = gpio_led_release,
};

/* 加载函数 */
static int __init gpio_led_init(void)
{
	int ret;

	ret = alloc_chrdev_region(&devon, 0, LED_COUNT, "gpio_led");
	if(ret < 0)
		return -1;

	/* 初始化cdev, 并将设备注册到内核 */
	cdev_init(&led_cdev, &gpio_led_fops);
	ret = cdev_add(&led_cdev, devon, LED_COUNT);
	if(ret < 0)
		goto ERR_CDEV_ADD;

	/* 创建设备类(/sys/class下可查看) */
	dev_class = class_create(THIS_MODULE, "led_class");
	if(IS_ERR(dev_class))
	{
		goto ERR_CLASS_CREATE;
	}

	/* 创建设备文件(/dev下可查看) */
	dev_led = device_create(dev_class, NULL, devon, NULL, "led");
	if(IS_ERR(dev_led))
		goto ERR_DEV_CREATE;

	printk("%s successfully\n", __FUNCTION__);
	return 0;

	ERR_DEV_CREATE:
	class_destroy(dev_class);
	
	ERR_CLASS_CREATE:
	cdev_del(&led_cdev);
	
	ERR_CDEV_ADD:
	unregister_chrdev_region(devon, LED_COUNT);

	return -1;
}

/* 卸载函数 */
static void __exit gpio_led_exit(void)
{
	
	/* 注销设备 */
	device_destroy(dev_class, devon);

	/* 注销设备类 */
	class_destroy(dev_class);

	/* 注销cdev */
	cdev_del(&led_cdev);

	/* 注销设备号 */
	unregister_chrdev_region(devon, LED_COUNT);

	printk("%s~\n", __FUNCTION__);
}

module_init(gpio_led_init);
module_exit(gpio_led_exit);

MODULE_AUTHOR("zengzr");
MODULE_LICENSE("GPL v2");


 

2、测试应用源码:

如何测试?流程如下:先打开设备,再对4个LED进行开1秒关1秒,循环5次,最后关闭设备,退出。

可在板上观察LED闪烁状态。

led_test.c:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

/* 设备文件名 */
#define LED_DEV_NAME    "/dev/led"

/* 控制命令 */
enum {
    LED_ALL_ON,
    LED_ALL_OFF,
};

/* LED测试程序: 开关5次 */
int main(void)
{
    int fd = 0;
    int flag = 0;

    fd = open(LED_DEV_NAME, O_RDWR);
    if(fd < 0)
    {
        printf("%d: open failed!\n", fd);
        return -1;
    }

    while(flag < 5)
    {
        ioctl(fd, LED_ALL_ON, 0);
        sleep(1);
        ioctl(fd, LED_ALL_OFF, 0);
        sleep(1);
        flag++;
    }

    close(fd);

    return 0;
}

 

3、Makefile

# make to build modules

obj-m := gpio_led_drv.o

KERNELDIR ?= /data/arm-linux/kernel/tiny4412/linux-3.5
PWD := $(shell pwd)

all: modules

modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:
	rm -rf *.o *.ko *mod* *.sy* *ord* .*cmd .tmp*

 

完!

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值