我們一般拿到一款全新的芯片,第一個要做的事情的就是驅動其 GPIO,控制其 GPIO 輸出高低電平,我們學習 I.MX6U 也一樣的,先來學習一下 I.MX6U 的 GPIO。在學習 I.MX6U的 GPIO 之前,我們先來回顧一下 STM32 的 GPIO 初始化(如果沒有學過 STM32 就不用回顧了),我們以最常見的 STM32F103 為例來看一下 STM32 的 GPIO 初始化,示例代碼如下:
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//使能 PB 端口時鐘
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //PB5 端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽輸出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO 口速度
GPIO_Init(GPIOB, &GPIO_InitStructure); //根據設定參數初始化 GPIOB.5
GPIO_SetBits(GPIOB,GPIO_Pin_5); //PB.5 輸出高
}
上述代碼就是使用庫函數來初始化 STM32 的一個 IO 為輸出功能,可以看出上述初始化代碼中重點要做的事情有一下幾個:
①、使能指定 GPIO 的時鐘。
②、初始化 GPIO,比如輸出功能、上拉、速度等等。
③、 STM32 有的 IO 可以作為其它外設引腳,也就是 IO 復用,如果要將 IO 作為其它外設引腳使用的話就需要設置 IO 的復用功能。
④、最后設置 GPIO 輸出高電平或者低電平。
STM32 的 GPIO 初始化就是以上四步,那么會不會也適用于 I.MX6U 的呢? I.MX6U 的GPIO 是不是也需要開啟相應的時鐘?是不是也可以設置復用功能?是不是也可以設置輸出或輸入、上下拉、速度等等這些?我們現在都不知道,只有去看 I.MX6U 的數據手冊和參考手冊才能知道,帶著上面四個疑問打開這兩份手冊,然后就是“啃”手冊。
可以看到LED掛在GPIO1_IO03上,輸出高電平就是熄滅LED燈,輸出低電平就是點亮LED燈
對于imx6ull我們基本上在LED上會用到以下章節的內容:
CCM: Clock Controller Module (時鐘控制模塊) - 章節18
IOMUXC : IOMUX Controller,IO復用控制器 - 章節32
GPIO: General-purpose input/output,通用的輸入輸出口 - 章節28
參考資料:芯片手冊《Chapter 28: General Purpose Input/Output (GPIO)》
有5組GPIO(GPIO1~GPIO5),每組引腳最多有32個,但是可能實際上并沒有那么多。
GPIO1有32個引腳:GPIO1_IO0~GPIO1_IO31;
GPIO2有22個引腳:GPIO2_IO0~GPIO2_IO21;
GPIO3有29個引腳:GPIO3_IO0~GPIO3_IO28;
GPIO4有29個引腳:GPIO4_IO0~GPIO4_IO28;
GPIO5有12個引腳:GPIO5_IO0~GPIO5_IO11;
GPIO的控制涉及4大模塊:CCM、IOMUXC、GPIO模塊本身,框圖如下:
參考資料:芯片手冊《Chapter 18: Clock Controller Module (CCM)》
GPIOx要用CCM_CCGRy寄存器中的2位來決定該組GPIO是否使能。哪組GPIO用哪個CCM_CCGR寄存器來設置,請看上圖紅框部分。
CCM_CCGR寄存器中某2位的取值含義如下:
① 00:該GPIO模塊全程被關閉
② 01:該GPIO模塊在CPU run mode情況下是使能的;在WAIT或STOP模式下,關閉
③ 10:保留
④ 11:該GPIO模塊全程使能
GPIO2時鐘控制:
GPIO1時鐘控制:
GPIO3時鐘控制:
GPIO4時鐘控制:
參考資料:芯片手冊《Chapter 32: IOMUX Controller (IOMUXC)》
對于某個/某組引腳,IOMUXC中有2個寄存器用來設置它:
① 選擇功能:
IOMUXC_SW_MUX_CTL_PAD_<PADNAME> :Mux pad xxx,選擇某個pad的功能
IOMUXC_SW_MUX_CTL_GRP_<GROUP NAME>:Mux grp xxx,選擇某組引腳的功能
某個引腳,或是某組預設的引腳,都有8個可選的模式(alternate (ALT) MUX_MODE)。
某個引腳,或是某組預設的引腳,都有8個可選的模式(alternate (ALT) MUX_MODE)。
比如:
② 設置上下拉電阻等參數:
IOMUXC_SW_PAD_CTL_PAD_<PAD_NAME>:pad pad xxx,設置某個pad的參數
IOMUXC_SW_PAD_CTL_GRP_<GROUP NAME>:pad grp xxx,設置某組引腳的參數
比如:
我們暫時只需要關心3個寄存器:
① GPIOx_GDIR:設置引腳方向,每位對應一個引腳,1-output,0-input
② GPIOx_DR:設置輸出引腳的電平,每位對應一個引腳,1-高電平,0-低電平
③ GPIOx_PSR:讀取引腳的電平,每位對應一個引腳,1-高電平,0-低電平
GPIO1Memory map如下:
翻譯一下:
① 設置CCM_CCGRx寄存器中某位使能對應的GPIO模塊// 默認是使能的,上圖省略了
② 設置IOMUX來選擇引腳用于GPIO
③ 設置GPIOx_GDIR中某位為0,把該引腳設置為輸入功能
④ 讀GPIOx_DR或GPIOx_PSR得到某位的值(讀GPIOx_DR返回的是GPIOx_PSR的值)
翻譯一下:
① 設置CCM_CCGRx寄存器中某位使能對應的GPIO模塊// 默認是使能的,上圖省略了
② 設置IOMUX來選擇引腳用于GPIO
③ 設置GPIOx_GDIR中某位為1,把該引腳設置為輸出功能
④ 寫GPIOx_DR某位的值
Linux 下的任何外設驅動,最終都是要配置相應的硬件寄存器。所以本章的 LED 燈驅動最終也是對 I.MX6ULL 的 IO 口進行配置,與裸機實驗不同的是,在 Linux 下編寫驅動要符合 Linux的驅動框架。I.MX6U-ALPHA 開發板上的 LED 連接到 I.MX6ULL 的 GPIO1_IO03 這個引腳上,因此本章實驗的重點就是編寫 Linux 下 I.MX6UL 引腳控制驅動。
在編寫驅動之前,我們需要先簡單了解一下 MMU 這個神器, MMU 全稱叫做 MemoryManage Unit,也就是內存管理單元。在老版本的 Linux 中要求處理器必須有 MMU,但是現在Linux 內核已經支持無 MMU 的處理器了。 MMU 主要完成的功能如下:
①、完成虛擬空間到物理空間的映射。
②、內存保護,設置存儲器的訪問權限,設置虛擬存儲空間的緩沖特性。
我們重點來看一下第①點,也就是虛擬空間到物理空間的映射,也叫做地址映射。首先了解兩個地址概念:虛擬地址(VA,Virtual Address)、物理地址(PA, Physcical Address)。對于 32 位的處理器來說,虛擬地址范圍是 2^32=4GB,我們的開發板上有 512MB 的 DDR3,這 512MB 的內存就是物理內存,經過 MMU 可以將其映射到整個 4GB 的虛擬空間 ,如圖:
物理內存只有 512MB,虛擬內存有 4GB,那么肯定存在多個虛擬地址映射到同一個物理地址上去,虛擬地址范圍比物理地址范圍大的問題處理器自會處理,這里我們不要去深究,因為MMU 是很復雜的一個東西 。
Linux 內核啟動的時候會初始化 MMU,設置好內存映射,設置好以后 CPU 訪問的都是虛擬 地 址 。 比 如 I.MX6ULL 的 GPIO1_IO03 引 腳 的 復 用 寄 存 器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 的地址為 0X020E0068。如果沒有開啟 MMU 的話直接向 0X020E0068 這個寄存器地址寫入數據就可以配置 GPIO1_IO03 的復用功能?,F在開啟了 MMU,并且設置了內存映射,因此就不能直接向 0X020E0068 這個地址寫入數據了。我們必須得到 0X020E0068 這個物理地址在 Linux 系統里面對應的虛擬地址,這里就涉及到了物理內存和虛擬內存之間的轉換,需要用到兩個函數: ioremap 和 iounmap。
ioremap 函 數 用 于 獲 取 指 定 物 理 地 址 空 間 對 應 的 虛 擬 地 址 空 間 , 定 義 在arch/arm/include/asm/io.h 文件中,定義如下:
#define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE)
void __iomem *__arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype)
{
return arch_ioremap_caller(phys_addr, size, mtype,
__builtin_return_address(0));
}
ioremap 是個宏,有兩個參數: cookie 和 size,真正起作用的是函數__arm_ioremap,此函數有三個參數和一個返回值,這些參數和返回值的含義如下:
phys_addr:要映射給的物理起始地址。
size:要映射的內存空間大小。
mtype: ioremap 的類型,可以選擇 MT_DEVICE、 MT_DEVICE_NONSHARED、MT_DEVICE_CACHED 和 MT_DEVICE_WC, ioremap 函數選擇 MT_DEVICE。
返回值: __iomem 類型的指針,指向映射后的虛擬空間首地址。
假如我們要獲取 I.MX6ULL 的 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 寄存器對應的虛擬地址,使用如下代碼即可:
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
static void __iomem* SW_MUX_GPIO1_IO03;
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
宏 SW_MUX_GPIO1_IO03_BASE 是寄存器物理地址, SW_MUX_GPIO1_IO03 是映射后的虛擬地址。對于 I.MX6ULL 來說一個寄存器是 4 字節(32 位)的,因此映射的內存長度為 4。映射完成以后直接對SW_MUX_GPIO1_IO03 進行讀寫操作即可。
卸載驅動的時候需要使用 iounmap 函數釋放掉 ioremap 函數所做的映射, iounmap 函數原型如下:
#define iounmap __arm_iounmap
void __arm_iounmap(volatile void __iomem *io_addr)
{
arch_iounmap(io_addr);
}
iounmap 只有一個參數 addr,此參數就是要取消映射的虛擬地址空間首地址。假如我們現在要取消掉 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 寄存器的地址映射,使用如下代碼即可:
iounmap(SW_MUX_GPIO1_IO03);
這里說的 I/O 是輸入/輸出的意思,并不是我們學習單片機的時候講的 GPIO 引腳。這里涉及到兩個概念: I/O 端口和 I/O 內存。當外部寄存器或內存映射到 IO 空間時,稱為 I/O 端口。當外部寄存器或內存映射到內存空間時,稱為 I/O 內存。但是對于 ARM 來說沒有 I/O 空間這個概念,因此 ARM 體系下只有 I/O 內存(可以直接理解為內存)。使用 ioremap 函數將寄存器的物理地址映射到虛擬地址以后,我們就可以直接通過指針訪問這些地址,但是 Linux 內核不建議這么做,而是推薦使用一組操作函數來對映射后的內存進行讀寫操作。
讀操作函數有如下幾個:
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)
readb、 readw 和 readl 這三個函數分別對應 8bit、 16bit 和 32bit 讀操作,參數 addr 就是要讀取寫內存地址,返回值就是讀取到的數據。
void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)
writeb、 writew 和 writel 這三個函數分別對應 8bit、 16bit 和 32bit 寫操作,參數 value 是要寫入的數值, addr 是要寫入的地址。
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define LED_MAJOR 200
#define LED_NAME "led"
static struct class *led_class;
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;
static int led_open(struct inode *inode, struct file *filp)
{
printk("led_open\r\n");
return 0;
}
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
printk("led_read\r\n");
return 0;
}
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
printk("led_write\r\n");
int retvalue;
u32 val = 0;
unsigned char databuf[1];
unsigned char ledstat;
retvalue = copy_from_user(databuf, buf, cnt);
if(retvalue < 0) {
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstat = databuf[0];
printk("ledstat:%d\r\n",ledstat);
if(ledstat == 1)
{
val = readl(GPIO1_DR);
val &= ~(1 << 3);
writel(val, GPIO1_DR);
}
else if(ledstat == 0)
{
val = readl(GPIO1_DR);
val|= (1 << 3);
writel(val, GPIO1_DR);
}
return 1;
}
static int led_release(struct inode *inode, struct file *filp)
{
printk("led_release\r\n");
return 0;
}
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
static int __init led_driver_init(void)
{
u32 val = 0;
int retvalue = 0;
printk("led_driver_init\r\n");
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26);
val |= (3 << 26);
writel(val, IMX6U_CCM_CCGR1);
writel(5, SW_MUX_GPIO1_IO03);
writel(0x10B0, SW_PAD_GPIO1_IO03);
val = readl(GPIO1_GDIR);
val &= ~(1 << 3);
val |= (1 << 3);
writel(val, GPIO1_GDIR);
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
if(retvalue < 0){
printk("register chrdev failed!\r\n");
return -EIO;
}
led_class = class_create(THIS_MODULE,"led_class");
device_create(led_class,NULL,MKDEV(LED_MAJOR,0),NULL,"led"); /* /dev/led */
return 0;
}
static void __exit led_driver_exit(void)
{
printk("led_driver_exit\r\n");
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
device_destroy(led_class,MKDEV(LED_MAJOR,0));
class_destroy(led_class);
unregister_chrdev(LED_MAJOR, LED_NAME);
}
module_init(led_driver_init);
module_exit(led_driver_exit);
MODULE_LICENSE("GPL");
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
int main(int argc, char *argv[])
{
int fd;
int ret;
uint8_t led;
fd = open(argv[1], O_RDWR);
if(!strcmp("led_on",argv[2]))
{
printf("led on\r\n");
led = 1;
write(fd,&led,sizeof(led));
}
if(!strcmp("led_off",argv[2]))
{
led = 0;
printf("led on\r\n");
write(fd,&led,sizeof(led));
}
close(fd);
}
測試方法:
點亮 LED燈 ./test_app /dev/led led_on
熄滅 LED燈 ./test_app /dev/led led_on
KERNELDIR := /home/zhongjun/project/board/yuanzi/imx6ull/nfs/kernel
CURRENT_PATH := $(shell pwd)
obj-m := led_drv.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) $(KBUILD_CFLAGS) M=$(CURRENT_PATH) modules
$(CROSS_COMPILE)gcc -o test_app test_app.c
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
rm -rf test_app
參考:
1.https://weidongshan.blog.csdn.net/article/details/122475478
2.【韋東山】嵌入式Linux應用開發完全手冊V4.0_韋東山全系列視頻文檔-IMX6ULL開發板.docx
3.【正點原子】I.MX6U嵌入式Linux驅動開發指南V1.4.pdf