天天看點

Linux驅動:Tiny4412開發闆上點亮LED燈程式(GPIO驅動)

上篇博文以globalmem為例實作了一個虛拟的字元裝置驅動,本文将在上文的基礎上,以點亮LED執行個體來介紹GPIO字元裝置驅動,将不重複上篇相同内容。

環境:主機-Ubuntu 16.04,開發闆-友善之臂tiny4412開發闆,核心版本linux-3.5,參考tiny4412相關手冊。

闆上硬體資源:

Linux驅動:Tiny4412開發闆上點亮LED燈程式(GPIO驅動)

注:實踐發現,本開發闆實際使用的分别是: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相關寄存器主要有以下幾種:

寄存器 描述 備注
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 掉電模式上下拉寄存器,配置上下拉狀态

還有關鍵的,寄存器位址及其較長的描述,如下:

1、配置寄存器GPxxCON:

Linux驅動:Tiny4412開發闆上點亮LED燈程式(GPIO驅動)
Linux驅動:Tiny4412開發闆上點亮LED燈程式(GPIO驅動)

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

一個寄存器是4個位元組32bit大小,按位定義每4bit一組共8組分别對應GPM4_0~7等8個IO。(上圖不完整)

再看右邊的數值定義,0x0 = Input, 0x1=Output ...等已說明很具體了,隻不過是給對應的位指派這麼簡單。

2、資料寄存器GPxxDAT:

Linux驅動:Tiny4412開發闆上點亮LED燈程式(GPIO驅動)

同理,[0~7]位分别對GPM4_0~7,還說:當配置成輸入時,讀取對應位的值就是對應IO的狀态了;配置成輸出時,寫入對應位的值就是對應IO的狀态了;當配置成功能引腳時如(UART),其值是未定義的。

3、上下拉寄存器GPxxPUD:

Linux驅動:Tiny4412開發闆上點亮LED燈程式(GPIO驅動)

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*
           

完!

繼續閱讀