imx6ull-qemu 裸機教程1:GPIO,IOMUX,I2C

無意間搜到了韋東山老師的6ul網站,上面有一個6ul的qemu仿真器,下載下來用了用,非常好用,有UI,比原裝的qemu-system-arm提供的6ul開發板多了很多功能。
下面貼出的就是韋東山老師的qemu網站:
百問網imx6ull-qemu
但是默認的跑了linux,沒有裸機的例程。所以本文寫了幾個裸機的程序以供參考學習6ul soc上一些外設IP。目的是以最簡單的代碼來幫助對6ul感興趣的朋友屬性IP的使用。
本教程源碼
目標實現以下模塊的裸機驅動教程:

  • GPIO LED
  • GPIO BUTTON
  • I2C AT24CXX EEPROM
  • GPT定時器
  • USDHC SD卡

注:本文中的驅動只適用於QEMU仿真器上使用,不一定能在真實的芯片上跑起來。原因主要是跟clock,timing有關。因爲qemu是純軟件的東西,qemu並沒有對timing有嚴格的要求,所以在本文中所有驅動都沒有對clock和timing進行處理。

1. 6UL SOC 啓動代碼編寫

1.1 System memory map

首先看看6UL SOC的memory map
在這裏插入圖片描述在這裏插入圖片描述
我們關心如下幾段內存空間:

  • 0x0000_0000 - 0x0001_7FFF BootROM。 6ul真實芯片啓動的第一條代碼是從BootROM 0地址啓動,6ul qemu第一條指令也是從0地址開始啓動。但是不同的是,6ul qemu並沒有BootROM的啓動代碼,所以我們編寫的裸機程序的代碼從0地址開始鏈接,這段ROM空間我們可以用作text段。在真實的芯片上,BootROM會從不同的啓動設備上讀出Boot Image,典型的如Uboot,然後把Boot Image扔到DDR或者OCRAM上去跑。
  • 0x0090_0000 - 0x0097_FFFF OCRAM。這段OCRAM我們可以用來存放data段,bss段和stack段。
  • 0x8000_0000 - 0xFFFF_FFFF DDR。這段內存可以用作通用內存,暫時在本文的代碼中沒用到。

1.2 鏈接文件

6ul_bare_metal/6ul_bare_metal.ld

ENTRY(reset)
SECTIONS
{
	. = 0x00000000;
 	.startup . : { start.o(.text) }
 	.text : { *(.text) }
    . = 0x00900000;
 	.data : { *(.data) }
 	.bss : { *(.bss COMMON) }
 	. = ALIGN(8);
 	. = . + 0x8000; /* 32kB of stack memory */
 	svc_stack_top = .;
}

正如上一節所述,test位於BootROM上,data,bss和stack段位於OCRAM上。其中:

  • start.o是啓動文件及異常向量表, 所以將其鏈接到最頂端。
  • stack佔用32KB大小內存。

1.3 啓動彙編代碼

6ul_bare_metal/start.S

.align 4
.global reset
.global c_entry
.section .isr_vector
.text
reset:
	B reset_handler
	B .
	B .             //SVC
	B .
	B .
	B .
	B .             //IRQ
	B .             //FIQ


reset_handler:
    ldr r0, =0x00900000
    mcr p15,0,r0,c12,c0,0
    ldr sp, =svc_stack_top
    bl c_entry
    b .

reset處存放了arm向量表,除了reset之外其他都是一個死循環。本文所有代碼作了以下簡化:

  • 不使用任何中斷,所有驅動都查詢中斷標誌位的方式。
  • 所有代碼運行在特權模式下。
    每一個demo都實現了一種外設,在運行模式下切換沒有什麼意義,增加了demo的複雜性,所以對所有裸機demo做了以上簡化。

啓動代碼很簡單,0地址就是一條B reset_handler的代碼,然後reset handler設了一下特權模式下的棧指針,就跳轉c函數的入口c_entry中。以下是start.s編譯器編出來的代碼。

00000000 <reset>:
   0:	ea000006 	b	20 <reset_handler>
   4:	eafffffe 	b	4 <reset+0x4>
   8:	eafffffe 	b	8 <reset+0x8>
   c:	eafffffe 	b	c <reset+0xc>
  10:	eafffffe 	b	10 <reset+0x10>
  14:	eafffffe 	b	14 <reset+0x14>
  18:	eafffffe 	b	18 <reset+0x18>
  1c:	eafffffe 	b	1c <reset+0x1c>

00000020 <reset_handler>:
  20:	e3a00609 	mov	r0, #9437184	; 0x900000
  24:	ee0c0f10 	mcr	15, 0, r0, cr12, cr0, {0}
  28:	e59fd004 	ldr	sp, [pc, #4]	; 34 <reset_handler+0x14>
  2c:	eb000034 	bl	104 <c_entry>
  30:	eafffffe 	b	30 <reset_handler+0x10>
  34:	00908008 	addseq	r8, r0, r8

使用cgdb debug啓動代碼如下:
在這裏插入圖片描述
打印的代碼涉及到UART,這裏先不解釋了,後面解釋UART的時候再說。代碼實現imx_uart.c和qemu_print.c中。

2 GPIO LED

2.1 IOMUXC

在這裏插入圖片描述
從原理圖上看到,我們要選擇點亮的LED使用了GPIO1_3這個pin。
在6UL上,每一個引腳可以有8個mux選項(也被稱作ALT模式)。比如我們要使用的GPIO1_3,這個pin有8個mux選項可供不同的IP使用。因此,我們再使用該pin前,要先選擇好特定的mux選項。
在這裏插入圖片描述
所以點亮LED前,我們先要寫好IOMUXC的代碼。很簡單就是封裝了一下IOMUXC寄存器的MUX_MODE和SION的操作。
imx_iomuxc.h

#ifndef __IMX6UL_IOMUXC_H__
#define __IMX6UL_IOMUXC_H__

#include <stdint.h>

#define SW_MUX_CTRL_PAD_MUX_MODE_MASK   0x00000007UL
#define SW_MUX_CTRL_PAD_MUX_MODE_SHIFT  0UL

#define SW_MUX_CTRL_PAD_SION_SHIFT      4UL
#define SW_MUX_CTRL_PAD_SION_MASK       (1UL << SW_MUX_CTRL_PAD_SION_SHIFT)
.......
static inline void iomuxc_set_daisy_in(uint32_t iomux_addr, uint8_t mode)
{
    *((volatile uint32_t *)iomux_addr) = 0;
    *((volatile uint32_t *)iomux_addr) = mode; 
}

static inline void iomuxc_enable_sion(uint32_t iomux_addr)
{
    *((volatile uint32_t *)iomux_addr) |= SW_MUX_CTRL_PAD_SION_MASK;
}

static inline void iomuxc_disable_sion(uint32_t iomux_addr)
{
    *((volatile uint32_t *)iomux_addr) &= ~SW_MUX_CTRL_PAD_SION_MASK;
}

static inline void iomuxc_set_mux(uint32_t iomux_addr, uint8_t mux_mode)
{
    *((volatile uint32_t *)iomux_addr) &= ~SW_MUX_CTRL_PAD_MUX_MODE_MASK;
    *((volatile uint32_t *)iomux_addr) |= SW_MUX_CTRL_PAD_MUX_MODE_MASK & mux_mode;
}

#endif

2.2 GPIO

GPIO 是通用輸入輸出端口的簡稱,簡單來說就是6UL可控制的引腳,6UL芯
片的GPIO 引腳與外部設備連接起來,從而實現與外部通訊、控制以及數據採集的功能。
6UL芯片的GPIO 被分成很多組,每組有32 個引腳.

[注:以下內容節選自《i.MX RT 庫開發實戰指南—基於野火RT1052 開發板》]
在這裏插入圖片描述
下面我們按圖 7-1 中的編號對GPIO 端口的結構部件進行說明。

  1. PAD
    PAD 代表了一個RT1052 的GPIO 引腳。在它的左側是一系列信號通道及控制線,如 input_on 控制輸入開關,Dir 控制引腳的輸入輸出方向,Data_out 控制引腳輸出高低電平,Data_in 作爲信號輸入,這些信號都經過一個IOMUX 的器件連接到左側的寄存器。另外,對於每個引腳都有很多關於屬性的配置,這些配置是由圖 7-2 中的框架結構實現的。
  2. IOMUX 複用選擇器
  3. Block 外設功能控制塊
    Block 是外設功能控制塊,例如具有ENET 的數據接收功能的引腳,它就需要網絡外設ENET 的支持,具有PWM輸出功能的引腳,它需要PWM外設的支持,這些外設在芯片內部會有獨立的功能邏輯控制塊,這些控制塊通過IOMUX 的複用信號與IO 引腳相連。使用時通過IOMUX 選擇具體哪個外設連接到IO。
  4. GPIO 外設
    GPIO 模塊是每個IO 都具有的外設,它具有IO 控制最基本的功能,如輸出高低電平、檢測電平輸入等。它也佔用IOMUX 分配的複用信號,也就是說使用GPIO 模塊功能時同樣需要使用IOMUX 選中GPIO 外設。圖中的GPIO.DR、GPIO.GDIR、GPIO.PSR 等是指GPIO 外設相關的控制寄存器,它們分別是數據寄存器、方向寄存器以及引腳狀態寄存器,
    功能介紹如下:
    GPIO.GDIR 方向寄存器
    控制一個GPIO 引腳時,要先用GDIR 方向寄存器配置該引腳用於輸出電平信號還是用作輸入檢測。典型的例子是使用輸出模式可以控制LED 燈的亮滅,輸入模式時可以用來檢測按鍵是否按下。
    GDIR 寄存器的每一個數據位代表一個引腳的方向,對應的位被設置爲0 時該引腳爲輸入模式,被設置爲1 時該引腳爲輸出模式
    在這裏插入圖片描述GPIO.DR 數據寄存器
    DR 數據寄存器直接代表了引腳的電平狀態,它也使用1 個數據位表示1 個引腳的電平,每位用1 表示高電平,用0 表示低電平
    在這裏插入圖片描述
    GPIO.PSR 引腳狀態寄存器
    PSR 引腳狀態寄存器相當於DR 寄存器的簡化版,它僅在GDIR 方向寄存器設置爲輸入模式時有效,它的每個位表示一個引腳當前的輸入電平狀態。PSR 寄存器的權限是隻讀的,對它進行寫操作是無效的。

GPIO.ICR1 & GPIO.ICR2
這兩個寄存器決定了每一個引腳觸發中斷的方式,每一個引腳有兩個bit表示。因此一個32位的寄存器只能表示16個pin。ICR1控制GPIOX_0 - GPIOX_15. ICR2控制GPIOX_16 - GPIOX_31。
每一個GPIO有4種中斷觸發方式:

  • 低電平觸發
  • 高電平觸發
  • 上升沿觸發
  • 下降沿觸發
    在這裏插入圖片描述
    GPIO.IMR中斷屏蔽寄存器
    IMR中每一位控制一個pin的中斷屏蔽,當置位1後,該GPIO就不會觸發中斷。
    在這裏插入圖片描述
    GPIO.ISR中斷狀態寄存器
    ISR中每一個bit表示每一個pin是否有中斷觸發,當置爲1時,表示有中斷觸發,爲0則沒有中斷髮生。軟件往該位寫1則會清掉該中斷標誌位
    在這裏插入圖片描述
    GPIO的頭文件imx_gpio.h
    代碼很簡單,僅僅是定義了gpio的結構體和聲明瞭API。
#ifndef __IMX_GPIO_H__
#define __IMX_GPIO_H__

#include <stdint.h>

#define LOW_LEVEL_SENSITIVE 0
#define HIGH_LEVEL_SENSITIVE 1
#define RISING_EDGE 2
#define FALLING_EDGE 3

typedef struct imx_gpio_tag
{

    volatile uint32_t dr;
    volatile uint32_t gdir;
    volatile uint32_t psr;
    volatile uint32_t icr1;
    volatile uint32_t icr2;
    volatile uint32_t imr;
    volatile uint32_t isr;
    volatile uint32_t edge_sel;

} imx_gpio_t;

extern void gpio_set_dr(imx_gpio_t *, uint8_t);
extern void gpio_clr_dr(imx_gpio_t *, uint8_t);
extern void gpio_set_output(imx_gpio_t *, uint8_t);
extern void gpio_set_input(imx_gpio_t *, uint8_t);
extern uint8_t gpio_get_psr(imx_gpio_t *, uint8_t);
extern void gpio_set_int_cfg(imx_gpio_t *, uint8_t , uint8_t);
extern void gpio_mask_int(imx_gpio_t *, uint8_t);
extern void gpio_unmask_int(imx_gpio_t *, uint8_t);
extern uint32_t gpio_get_int_stat(imx_gpio_t *, uint8_t);
extern void gpio_clr_int_stat(imx_gpio_t *, uint8_t);
extern void gpio_set_edge_sel(imx_gpio_t *, uint8_t);
extern void gpio_clr_edge_sel(imx_gpio_t *, uint8_t);
extern void dump_gpio(imx_gpio_t *);

#endif

API實現也很簡單,就是對GPIO硬件進行了軟件封裝

#include "imx_gpio.h"
#include "imx_uart.h"

void gpio_set_dr(imx_gpio_t *gpio, uint8_t idx)
{
    gpio->dr |= (1 << idx);
}

void gpio_clr_dr(imx_gpio_t *gpio, uint8_t idx)
{
    gpio->dr &= ~(1 << idx);
}

void gpio_set_output(imx_gpio_t *gpio, uint8_t idx)
{
    gpio->gdir |= (1 << idx);
}

void gpio_set_input(imx_gpio_t *gpio, uint8_t idx)
{
    gpio->gdir &= ~(1 << idx);
}

uint8_t gpio_get_psr(imx_gpio_t *gpio, uint8_t idx)
{
    return (gpio->psr & (1 << idx));
}

void gpio_set_int_cfg(imx_gpio_t *gpio, uint8_t idx, uint8_t cfg)
{
    uint32_t shift = 0;
    uint32_t mask = 0;

    if (idx < 16)
    {
        shift = idx * 2;
        mask = 3 << (idx * 2);
        gpio->icr1 &= ~mask;
        gpio->icr1 |= (cfg << shift);
    }
    else
    {
        shift = (idx - 16) * 2;
        mask = 3 << ((idx - 16) * 2);
        gpio->icr2 &= ~mask;
        gpio->icr2 |= (cfg << shift);
    }
}

void gpio_mask_int(imx_gpio_t *gpio, uint8_t idx)
{
    gpio->imr &= ~(1 << idx);
}

void gpio_unmask_int(imx_gpio_t *gpio, uint8_t idx)
{
    gpio->imr |= (1 << idx);
}

uint32_t gpio_get_int_stat(imx_gpio_t *gpio, uint8_t idx)
{
    return gpio->isr & (1 << idx);
}

void gpio_clr_int_stat(imx_gpio_t *gpio, uint8_t idx)
{
    gpio->isr |= (1 << idx);
}

void gpio_set_edge_sel(imx_gpio_t *gpio, uint8_t idx)
{
    gpio->edge_sel |= (1 << idx);
}

void gpio_clr_edge_sel(imx_gpio_t *gpio, uint8_t idx)
{
    gpio->edge_sel &= ~(1 << idx);
}

void dump_gpio(imx_gpio_t *gpio)
{
    printf("%s isr:%x\n", __func__, gpio->isr);
    printf("%s icr1:%x\n", __func__, gpio->icr1);
    printf("%s icr2:%x\n", __func__, gpio->icr2);
    printf("%s dr:%x\n", __func__, gpio->dr);
    printf("%s gdir:%x\n", __func__, gpio->gdir);
    printf("%s imr:%x\n\n", __func__, gpio->imr);
}

測試代碼

測試代碼先配置GPIO1_3的IOMUX爲ALT5,然後就按時往GPIO1_3的DR的寄存器寫1和0翻轉LED即可。
entry.c

static void test_led()
{
    uint8_t i = 0;
    imx_gpio_t *gpio1 = (imx_gpio_t *)0x0209c000UL;

    iomuxc_set_mux(IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03, MUX_MODE_ALT5);

    gpio_set_output(gpio1, 3);

    while(1) {
        if ((i % 2) == 0)
            gpio_set_dr(gpio1, 3);
        else
            gpio_clr_dr(gpio1, 3);
        printf("%s:%d\n", __func__, i++);
        delay();
    }
}

運行命令: make run,運行結果如下圖
在這裏插入圖片描述

3 GPIO Button

在這裏插入圖片描述
Demo中使用GPIO1_18作爲KEY2。使用輪詢中斷標誌位的方式讀取按鍵事件,當按鍵被按下的時候,將LED狀態翻轉。

首先配置LED的GPIO1_3爲輸出

static void test_button()
{
    uint32_t button_int_stat = 0;
    uint8_t led_status = 0;
    imx_gpio_t *gpio1 = (imx_gpio_t *)0x0209c000UL;

    iomuxc_set_mux(IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03, MUX_MODE_ALT5);
    gpio_set_output(gpio1, 3);

配置KEY2的GPIO1_18爲輸入狀態,使能中斷但關閉中斷,上升沿觸發中斷。即使屏蔽中斷,當有上升沿事件觸發後中斷標誌位還是會被置上

    iomuxc_set_mux(IOMUXC_SW_MUX_CTL_PAD_UART1_CTS, MUX_MODE_ALT5);
    gpio_set_input(gpio1, 18);
    gpio_set_int_cfg(gpio1, 18, RISING_EDGE);
    gpio_clr_int_stat(gpio1, 18);
    gpio_unmask_int(gpio1, 18);

定時查詢KEY2的狀態,一旦KEY2被按下彈起一次,LED就會翻轉一次。

    while (1) {

        while((button_int_stat = gpio_get_int_stat(gpio1, 18)) == 0) {
            delay();
        }

        printf("button changed\n");
        led_status = ~led_status;
        gpio_clr_int_stat(gpio1, 18);
        if (led_status == 0) {
            gpio_set_dr(gpio1, 3);
        } else {
            gpio_clr_dr(gpio1, 3);
        }
        delay();
    }

代碼很簡單,直接make run
在這裏插入圖片描述

發佈了22 篇原創文章 · 獲贊 54 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章