無意間搜到了韋東山老師的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 端口的結構部件進行說明。
- PAD
PAD 代表了一個RT1052 的GPIO 引腳。在它的左側是一系列信號通道及控制線,如 input_on 控制輸入開關,Dir 控制引腳的輸入輸出方向,Data_out 控制引腳輸出高低電平,Data_in 作爲信號輸入,這些信號都經過一個IOMUX 的器件連接到左側的寄存器。另外,對於每個引腳都有很多關於屬性的配置,這些配置是由圖 7-2 中的框架結構實現的。 - IOMUX 複用選擇器
- Block 外設功能控制塊
Block 是外設功能控制塊,例如具有ENET 的數據接收功能的引腳,它就需要網絡外設ENET 的支持,具有PWM輸出功能的引腳,它需要PWM外設的支持,這些外設在芯片內部會有獨立的功能邏輯控制塊,這些控制塊通過IOMUX 的複用信號與IO 引腳相連。使用時通過IOMUX 選擇具體哪個外設連接到IO。 - 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