上篇博文以globalmem為例實作了一個虛拟的字元裝置驅動,本文将在上文的基礎上,以點亮LED執行個體來介紹GPIO字元裝置驅動,将不重複上篇相同内容。
環境:主機-Ubuntu 16.04,開發闆-友善之臂tiny4412開發闆,核心版本linux-3.5,參考tiny4412相關手冊。
闆上硬體資源:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnL3ETNyMTM0IjM2IjMwAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
注:實踐發現,本開發闆實際使用的分别是: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:
其位址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*
完!