上篇博文以globalmem为例实现了一个虚拟的字符设备驱动,本文将在上文的基础上,以点亮LED实例来介绍GPIO字符设备驱动,将不重复上篇相同内容。
环境:主机-Ubuntu 16.04,开发板-友善之臂tiny4412开发板,内核版本linux-3.5,参考tiny4412相关手册。
板上硬件资源:
注:实践发现,本开发板实际使用的分别是:GPM4_0、GPM4_1、GPM4_2、GPM4_3(可能硬件版本不一)
重点意外:Tiny4412自带内核已把LED驱动编进了内核,因此,需重新配置内核将其取消或编译成模块,再重新烧写内核。
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相关寄存器主要有以下几种:
寄存器 | 描述 | 备注 |
---|---|---|
GPxxCON | configuration register | 配置寄存器,配置输入/输出/IO复用等功能 |
GPxxDAT | data register | 数据寄存器,读取输入数据/设置输出数据 |
GPxxPUD | pull-up/down register | 上/下拉寄存器,配置上下拉状态 |
GPxxDRV | drive strength control register | 驱动强度控制寄存器,配置IO驱动能力 |
GPxxCONPDN | power down mode configuration register | 掉电模式配置寄存器,配置输入/输出 |
GPxxPUDPDN | power down mode pull-up/down register | 掉电模式上下拉寄存器,配置上下拉状态 |
还有关键的,寄存器地址及其详细描述,如下:
其地址Address就是:Base Address(基地址)+offset(偏移),即 0x1100 0000 + 0x02E0;
一个寄存器是4个字节32bit大小,按位定义每4bit一组共8组分别对应GPM4_0~7等8个IO。(上图不完整)
再看右边的数值定义,0x0 = Input, 0x1=Output ...等已说明很具体了,只不过是给对应的位赋值这么简单。
同理,[0~7]位分别对GPM4_0~7,还说:当配置成输入时,读取对应位的值就是对应IO的状态了;配置成输出时,写入对应位的值就是对应IO的状态了;当配置成功能引脚时如(UART),其值是未定义的。
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函数的方式,则需用到以下函数:
/* 申请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 控制。
gpio_led_drv.c:
#include
#include
#include
#include
#include
#include
#include
#include
//#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=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
如何测试?流程如下:先打开设备,再对4个LED进行开1秒关1秒,循环5次,最后关闭设备,退出。
可在板上观察LED闪烁状态。
led_test.c:
#include
#include
#include
#include
#include
#include
/* 设备文件名 */
#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;
}
# 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*
完!