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 端口的结构部件进行说明。
- 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
更多推荐
所有评论(0)