title: 裸机实验总结 tags: [裸机开发, IMX6ULL, 实验总结, 嵌入式Linux] created: 2026-09-16 updated: 2026-09-17 pdf_ref:
关联知识: [[01-Cortex-A7架构精讲]] | [[02-交叉编译工具链]] | [[03-IMX6ULL启动流程]] 本章目标: 系统梳理10个核心裸机实验,建立从汇编到C语言、从GPIO到复杂外设的完整认知链路
| 维度 | 裸机 (Bare Metal) | RTOS | Linux |
|---|---|---|---|
| 调度机制 | 无,main循环轮询 | 优先级抢占调度 | 多进程/多线程调度 |
| 内存管理 | 物理地址直接访问 | 堆+静态分区 | 虚拟内存+MMU |
| 外设访问 | 直接操作寄存器 | HAL/驱动封装 | 子系统框架 |
| 中断处理 | 直接注册ISR | 有限中断嵌套 | 上下半部机制 |
| 适用场景 | 简单控制、启动加载 | 实时控制、物联网 | 复杂应用、多媒体 |
| 开发复杂度 | 低 | 中 | 高 |
| 资源占用 | 最小(KB级) | 中等(几十KB) | 大(MB级) |
1. 查阅芯片手册 → 定位外设寄存器地址
2. 编写汇编启动文件 → 初始化C运行环境(栈、BSS段)
3. 编写C语言业务代码 → 外设初始化、功能实现
4. 编写链接脚本(.lds) → 定义内存布局
5. 编写Makefile → 编译、链接、生成.bin
6. 烧录验证 → 通过SD卡/串口下载运行
I.MX6ULL的GPIO系统由5组控制器组成:
| GPIO组 | IO数量 | 基地址 | 说明 |
|---|---|---|---|
| GPIO1 | 32个 | 0x0209C000 | GPIO1_IO00~IO09 在 IOMUXC 中有独立 GPIO 命名 |
| GPIO2 | 22个 | 0x020A0000 | 其余 IO 需先经 IOMUXC 复用(MUX_MODE=ALT5)为 GPIO |
| GPIO3 | 29个 | 0x020A4000 | |
| GPIO4 | 29个 | 0x020A8000 | |
| GPIO5 | 12个 | 0x020AC000 | 包含 SNVS 域 IO(如 SNVS_TAMPER1) |
5 组 GPIO 共 124 个 IO(32+22+29+29+12)。每组都有 8 个寄存器:
DR(数据)、GDIR(方向)、PSR(状态)、ICR1/ICR2(中断配置,每个 IO 2 位)、IMR(中断屏蔽)、ISR(中断状态,写 1 清零)、EDGE_SEL(边沿选择,置 1 为双边沿,覆盖 ICR)。ICR 触发方式:00 低电平、01 高电平、10 上升沿、11 下降沿。
GPIO初始化三步曲:
LED0连接到GPIO1_IO03,通过三极管驱动,低电平点亮。
.global _start
_start:
@ 使能GPIO1时钟:GPIO1时钟由 CCM_CCGR1 的 bit27:26 控制
@ 注意:0x020C4068 是 CCM_CCGR0,CCM_CCGR1 应为 0x020C406C
ldr r0, =0x020C406C @ CCM_CCGR1寄存器地址
ldr r1, [r0]
orr r1, r1, #(3 << 26) @ CG13 位[27:26]=11,使能GPIO1时钟
str r1, [r0]
@ (教材例程更简单:CCGR0~CCGR6 从 0x020C4068 起每 4 字节一个,全部写 0xFFFFFFFF)
@ 配置GPIO1_IO03复用为GPIO
ldr r0, =0x020E0068 @ IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03
mov r1, #5 @ ALT5 = GPIO模式
str r1, [r0]
@ 配置GPIO1_IO03电气属性
ldr r0, =0x020E02F4 @ IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03
ldr r1, =0x10B0
str r1, [r0]
@ 设置GPIO1_IO03为输出
ldr r0, =0x0209C004 @ GPIO1_GDIR寄存器
ldr r1, [r0]
orr r1, r1, #(1 << 3)
str r1, [r0]
@ 点亮LED(输出低电平)
ldr r0, =0x0209C000 @ GPIO1_DR寄存器
ldr r1, [r0]
bic r1, r1, #(1 << 3)
str r1, [r0]
loop:
b loop
LED0常亮,程序死循环保持状态。
启动文件 start.S:
.global _start
_start:
@ 进入SVC模式
mrs r0, cpsr
bic r0, r0, #0x1f
orr r0, r0, #0x13
msr cpsr, r0
ldr sp, =0x80200000 @ 设置栈指针
b main @ 跳转到main函数
main.h(以宏定义寄存器地址):
#define CCM_CCGR0 *((volatile unsigned int *)0x020C4068) /* 注意 CCGR0=0x020C4068 */
#define CCM_CCGR1 *((volatile unsigned int *)0x020C406C) /* CCGR1=0x020C406C */
#define CCM_CCGR2 *((volatile unsigned int *)0x020C4070)
#define CCM_CCGR3 *((volatile unsigned int *)0x020C4074)
#define CCM_CCGR4 *((volatile unsigned int *)0x020C4078)
#define CCM_CCGR5 *((volatile unsigned int *)0x020C407C)
#define CCM_CCGR6 *((volatile unsigned int *)0x020C4080)
#define SW_MUX_GPIO1_IO03 *((volatile unsigned int *)0x020E0068)
#define SW_PAD_GPIO1_IO03 *((volatile unsigned int *)0x020E02F4)
#define GPIO1_DR *((volatile unsigned int *)0x0209C000)
#define GPIO1_GDIR *((volatile unsigned int *)0x0209C004)
C语言主函数 main.c:
#include "main.h"
void clk_enable(void) /* 使能所有外设时钟 */
{
CCM_CCGR0 = 0xffffffff;
CCM_CCGR1 = 0xffffffff;
CCM_CCGR2 = 0xffffffff;
CCM_CCGR3 = 0xffffffff;
CCM_CCGR4 = 0xffffffff;
CCM_CCGR5 = 0xffffffff;
CCM_CCGR6 = 0xffffffff;
}
void led_init(void) /* 初始化 LED 所用 GPIO */
{
SW_MUX_GPIO1_IO03 = 0x5; /* 复用为 GPIO1_IO03(ALT5) */
SW_PAD_GPIO1_IO03 = 0x10B0; /* IO 电气属性 */
GPIO1_GDIR = 0x8; /* bit3=1,设置为输出 */
GPIO1_DR = 0x0; /* 输出低电平,打开 LED0 */
}
int main(void)
{
clk_enable(); /* 使能所有的时钟 */
led_init(); /* 初始化 LED */
while (1) {} /* 死循环 */
}
蜂鸣器通过PNP三极管8550驱动,SNVS_TAMPER1(GPIO5_IO01)控制:
void beep_init(void)
{
/* 1、复用为GPIO5_IO01 */
IOMUXC_SetPinMux(IOMUXC_SNVS_SNVS_TAMPER1_GPIO5_IO01, 0);
/* 2、配置电气属性 */
IOMUXC_SetPinConfig(IOMUXC_SNVS_SNVS_TAMPER1_GPIO5_IO01, 0X10B0);
/* 3、设置为输出 */
GPIO5->GDIR |= (1 << 1);
/* 4、默认高电平,关闭蜂鸣器 */
GPIO5->DR |= (1 << 1);
}
void beep_switch(int status)
{
if(status == ON)
GPIO5->DR &= ~(1 << 1); /* 打开蜂鸣器 */
else if(status == OFF)
GPIO5->DR |= (1 << 1); /* 关闭蜂鸣器 */
}
LED0与蜂鸣器同步闪烁,周期约500ms。
KEY0连接到UART1_CTS(GPIO1_IO18),外接10K上拉电阻:
通用GPIO驱动:
typedef enum _gpio_pin_direction {
kGPIO_DigitalInput = 0U,
kGPIO_DigitalOutput = 1U,
} gpio_pin_direction_t;
void gpio_init(GPIO_Type *base, int pin, gpio_pin_config_t *config)
{
if(config->direction == kGPIO_DigitalInput)
base->GDIR &= ~(1 << pin);
else {
base->GDIR |= (1 << pin);
gpio_pinwrite(base, pin, config->outputLogic);
}
}
int gpio_pinread(GPIO_Type *base, int pin)
{
return (((base->DR) >> pin) & 0x1);
}
按键驱动 bsp_key.c(教材第15章):
void key_init(void)
{
gpio_pin_config_t key_config;
/* 1、初始化 IO 复用,复用为 GPIO1_IO18 */
IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0);
/* 2、配置 UART1_CTS_B 的 IO 属性:0xF080
* bit[15:14]=11 默认 22K 上拉 bit[13]=1 pull bit[12]=1 pull/keeper 使能
* bit[5:3]=000 关闭输出 bit[0]=0 低转换率
*/
IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0xF080);
/* 3、初始化 GPIO,GPIO1_IO18 设置为输入 */
key_config.direction = kGPIO_DigitalInput;
gpio_init(GPIO1, 18, &key_config);
}
int key_getvalue(void) /* 0:无按键,其他值:对应按键值 */
{
int ret = 0;
static unsigned char release = 1; /* 按键已松开标志 */
if((release == 1) && (gpio_pinread(GPIO1, 18) == 0)) /* KEY0 按下 */
{
delay(10); /* 延时消抖约 10ms(教材第57行) */
release = 0; /* 标记按键按下 */
if(gpio_pinread(GPIO1, 18) == 0)
ret = KEY0_VALUE;
}
else if(gpio_pinread(GPIO1, 18) == 1) /* KEY0 未按下 */
{
ret = 0;
release = 1; /* 标记按键释放 */
}
return ret;
}
按键检测逻辑(main.c):
int main(void)
{
int keyvalue = 0;
unsigned char led_state = OFF;
unsigned char beep_state = OFF;
clk_enable();
led_init();
beep_init();
key_init();
while(1)
{
keyvalue = key_getvalue();
if(keyvalue)
{
switch (keyvalue)
{
case KEY0_VALUE:
beep_state = !beep_state; /* 翻转蜂鸣器 */
beep_switch(beep_state);
break;
}
}
delay(10);
}
}
说明:教材的
key_init把 UART1_CTS 配置为 22K 上拉(0xF080),key_getvalue用静态变量release记录按键是否已释放,并用delay(10)做约 10ms 的软件消抖——这是第19章「定时器按键消抖」要优化的对象。
按下KEY0切换蜂鸣器开关状态,实现按键控制。
GPT(General Purpose Timer)是32位向上计数器,特性:
void delay_init(void)
{
GPT1->CR = 0; /* 清零 */
GPT1->CR = 1 << 15; /* bit15=1 进入软复位 */
while((GPT1->CR >> 15) & 0x01); /* 等待复位完成 */
/* CLKSRC(bit8:6)=001 选择 ipg_clk=66MHz;FRR(bit9)=0 为 restart 模式 */
GPT1->CR = (1 << 6);
GPT1->PR = 65; /* 66 分频,GPT1 时钟 = 66M/(65+1)=1MHz */
GPT1->OCR[0] = 0xFFFFFFFF; /* 不使用比较中断,设为最大值 */
GPT1->CR |= 1 << 0; /* 使能 GPT1 */
}
/* 微秒级延时(教材 delayus) */
void delayus(unsigned int usdelay)
{
unsigned long oldcnt, newcnt;
unsigned long tcntvalue = 0; /* 走过的总时间 */
oldcnt = GPT1->CNT;
while(1)
{
newcnt = GPT1->CNT;
if(newcnt != oldcnt)
{
if(newcnt > oldcnt) /* 向上计数且未溢出 */
tcntvalue += newcnt - oldcnt;
else /* 发生溢出回滚 */
tcntvalue += 0xFFFFFFFF - oldcnt + newcnt;
oldcnt = newcnt;
if(tcntvalue >= usdelay) /* 延时时间到 */
break;
}
}
}
/* 毫秒级延时(教材 delayms) */
void delayms(unsigned int msdelay)
{
int i = 0;
for(i = 0; i < msdelay; i++)
delayus(1000);
}
延时精度达到1μs级别,比循环延时准确得多。
| 延时方式 | 精度 | 原理 | 适用场景 |
|---|---|---|---|
| 循环延时 | ms级 | 编译器优化影响大 | 仅用于简单等待 |
| SysTick | μs级 | Cortex-M专用 | STM32系列 |
| GPT定时器 | 1μs | 硬件计数(读 GPTx_CNT) | I.MX6U 高精度延时(本章选用) |
| EPIT定时器 | — | 32位向下计数 + 比较中断 | 周期中断、定时器消抖,不做延时 |
| 类型 | 全称 | 特点 | 代表芯片 |
|---|---|---|---|
| SRAM | Static RAM | 不需刷新,速度快,成本高 | IS62WV51216 |
| SDRAM | Synchronous DRAM | 同步时钟,需刷新,成本低 | W9825G6KH |
| DDR | Double Data Rate | 双沿采样,带宽翻倍 | — |
| DDR2 | DDR2 SDRAM | 4bit预取,1.8V | — |
| DDR3 | DDR3 SDRAM | 8bit预取,1.5V,自动校准 | MT41K256M16 |
| DDR4 | DDR4 SDRAM | 16bit预取,1.2V | — |
I.MX6U Boot ROM会读取IVT中的DCD(Data Configuration Data)表,自动完成DDR3初始化:
MMDC = 0x021B0000。MMDC 包含内核(MMDC_CORE)和 PHY(MMDC_PHY),PHY 负责时序调整与校准,保证数据在 400MHz 被正确采样。CCM_CBCMR.PRE_PERIPH2_CLK_SEL(bit22:21)=01 → 选 PLL2_PFD2 作为 pre_periph2;CCM_CBCDR.PERIPH2_CLK_SEL(bit26)=0 → periph2_clk 选 pll2_main_clk;CCM_CBCDR.FABRIC_MMDC_PODF(bit5:3)=0 → 1 分频,得到 MMDC 时钟 ≈396MHz。NXP 官方工具流程(教材第23章):
I.MX6UL_DDR3_Script_Aid_V0.02.xlsx 填写 DDR3 参数(容量、位宽、行/列地址、tRCD/tRC/tRAS 等),生成 .inc 初始化脚本(全是“寄存器地址=寄存器值”);ddr_stress_tester(DDR_Tester.exe)经 USB OTG 把脚本下载到开发板;.inc 后重新加载、下载;校准得到的 6 个寄存器(教材示例代码 23.5.3.1):
| 寄存器名 | 地址 |
|---|---|
| MMDC_MPWLDECTRL0 | 0x021B080C |
| MMDC_MPWLDECTRL1 | 0x021B0810 |
| MPDGCTRL0 PHY0 | 0x021B083C |
| MPDGCTRL1 PHY0 | 0x021B0840 |
| MPRDDLCTL PHY0 | 0x021B0848 |
| MPWRDLCTL PHY0 | 0x021B0850 |
DDR3 关键时间参数(NT5CC256M16EP-EK): tRCD=tRP=CL=13.91ns(CL-tRCD-tRP = 13-13-13)、tRC=47.91ns、tRAS=34ns。
DDR3初始化成功后,程序可使用0x80000000~0xA0000000地址范围(512MB)。
分辨率: 1080P=1920×1080,2K=2560×1440
像素格式:
| 格式 | 位宽 | 字节 | 说明 |
|---|---|---|---|
| RGB565 | 16bit | 2B | R5+G6+B5,STM32常用 |
| RGB888 | 24bit | 3B | R8+G8+B8 |
| ARGB8888 | 32bit | 4B | A8+R8+G8+B8,本实验使用 |
RGB LCD信号线:
| 信号线 | 说明 |
|---|---|
| R[7:0]/G[7:0]/B[7:0] | 24根数据线 |
| DE | 数据使能 |
| VSYNC | 垂直同步 |
| HSYNC | 水平同步 |
| PCLK | 像素时钟 |
eLCDIF 基地址 LCDIF = 0x021C8000,支持 MPU / VSYNC / DOTCLK 三种接口,驱动 RGB 屏使用 DOTCLK 接口。
| 寄存器 | 作用 |
|---|---|
LCDIF_CTRL |
控制:SFTRST(bit31) 软复位、MASTER(bit5) 主模式、RUN(bit0) 使能、DATA_FORMAT_16_BIT(bit3) 选 ARGB555/RGB565、bit11:10 总线宽度、bit9:8 数据宽度 |
LCDIF_CTRL1 |
BYTE_PACKING_FORMAT(bit19:16),ARGB8888 下设为 0x7 |
LCDIF_TRANSFER_COUNT |
bit31:16 = 帧高 V_COUNT,bit15:0 = 帧宽 H_COUNT |
LCDIF_VDCTRL0 |
VSYNC/DOTCLK 控制:VSYNC_PULSE_WIDTH(bit17:0) 设置 VSPW,bit28 使能 ENABLE 输出等 |
LCDIF_VDCTRL1 |
VSYNC 总周期 = 屏高 + VSPW + VBP + VFP |
LCDIF_VDCTRL2 |
bit31:18 = HSPW;bit17:0 = HSYNC 总周期 = 屏宽 + HSPW + HBP + HFP |
LCDIF_VDCTRL3 |
bit27:16 = HBP+HSPW;bit15:0 = VBP+VSPW |
LCDIF_VDCTRL4 |
bit18 置 1(使用 VSYNC/HSYNC/DOTCLK);bit17:0 = 屏宽 |
LCDIF_CUR_BUF / LCDIF_NEXT_BUF |
当前/下一帧缓存地址 |
| 参数 | 含义 | 值 | | ---- | ---- | -- | | width / height | 屏宽 / 屏高 | 1024 / 600 | | pixsize | 每像素字节数(ARGB8888) | 4 | | hspw | HSYNC 信号宽度 | 20 | | hbpd | 水平同步后肩 HBP | 140 | | hfpd | 水平同步前肩 HFP | 160 | | vspw | VSYNC 信号宽度 | 3 | | vbpd | 帧同步后肩 VBP | 20 | | vfpd | 帧同步前肩 VFP | 12 | | framebuffer | 显存首地址 | 0x89000000 |
帧周期:T = (VSPW+VBP+LINE+VFP) × (HSPW+HBP+HVALID+HFP)。
#define LCD_FRAMEBUF_ADDR (0x89000000) /* LCD 显存地址 */
struct tftlcd_typedef {
unsigned short height, width;
unsigned char pixsize;
unsigned short vspw, vbpd, vfpd;
unsigned short hspw, hbpd, hfpd;
unsigned int framebuffer, forecolor, backcolor;
} tftlcd_dev;
void lcd_init(void)
{
lcdgpio_init(); /* 初始化 IO,复用为 eLCDIF */
lcdclk_init(32, 3, 5); /* 初始化 LCD 时钟 */
lcd_reset(); delayms(10); lcd_noreset();
tftlcd_dev.height = 600;
tftlcd_dev.width = 1024;
tftlcd_dev.pixsize = 4; /* ARGB8888,每像素 4 字节 */
tftlcd_dev.vspw = 3; tftlcd_dev.vbpd = 20; tftlcd_dev.vfpd = 12;
tftlcd_dev.hspw = 20; tftlcd_dev.hbpd = 140; tftlcd_dev.hfpd = 160;
tftlcd_dev.framebuffer = LCD_FRAMEBUF_ADDR;
LCDIF->CTRL |= (1<<19)|(1<<17)|(3<<10)|(3<<8)|(1<<5); /* DOTCLK、24位总线/数据、主模式 */
LCDIF->CTRL1 = 0x7 << 16; /* ARGB 模式,A 通道不传输 */
LCDIF->TRANSFER_COUNT = (tftlcd_dev.height << 16) | tftlcd_dev.width;
LCDIF->VDCTRL0 = (1<<28)|(1<<24)|(1<<21)|(1<<20)|(tftlcd_dev.vspw << 0);
LCDIF->VDCTRL1 = tftlcd_dev.height + tftlcd_dev.vspw + tftlcd_dev.vfpd + tftlcd_dev.vbpd;
LCDIF->VDCTRL2 = (tftlcd_dev.hspw << 18) |
(tftlcd_dev.width + tftlcd_dev.hspw + tftlcd_dev.hfpd + tftlcd_dev.hbpd);
LCDIF->VDCTRL3 = ((tftlcd_dev.hbpd + tftlcd_dev.hspw) << 16) |
(tftlcd_dev.vbpd + tftlcd_dev.vspw);
LCDIF->VDCTRL4 = (1 << 18) | tftlcd_dev.width;
LCDIF->CUR_BUF = LCDIF->NEXT_BUF = (unsigned int)tftlcd_dev.framebuffer;
lcd_enable(); /* LCDIF->CTRL |= (1 << 0) */
}
/* 绘制像素点(ARGB8888,每像素 4 字节) */
void lcd_drawpoint(unsigned int x, unsigned int y, unsigned int color)
{
*(unsigned int *)(tftlcd_dev.framebuffer + 4 * (y * tftlcd_dev.width + x)) = color;
}
/* 填充矩形 */
void lcd_fill(unsigned int x0, unsigned int y0,
unsigned int x1, unsigned int y1, unsigned int color)
{
unsigned int x, y;
for(y = y0; y < y1; y++)
for(x = x0; x < x1; x++)
lcd_drawpoint(x, y, color);
}
显存 0x89000000 对 256MB(0x80000000~0x90000000)和 512MB(0x80000000~0xA0000000)两款 DDR 都适用。教材另提供
bsp_lcdapi.c,封装lcd_show_string、lcd_shownum等字符/数字显示函数(依赖字库)。
LCD屏幕显示彩色测试图案和字符信息。
I.MX6U的RTC位于SNVS(Secure Non-Volatile Storage)模块内:
SNVS 基地址 SNVS = 0x020CC000,SRTC 位于 SNVS_LP。相关寄存器:SNVS_HPCOMR(NPSWA_EN bit31)、SNVS_LPCR(SRTC_ENV bit0)、SNVS_LPSRTCMR/SNVS_LPSRTCLR(秒数)。
/* 教材 bsp_rtc.c(I.MX6ULL 的 MCIMX6Y2.h 需手动补齐 SNVS_Type 寄存器) */
void rtc_init(void)
{
/* HPCOMR 的 NPSWA_EN(bit31) 必须置 1,否则非特权软件无法访问 SNVS 寄存器 */
SNVS->HPCOMR |= (1 << 31);
/* 首次使用可设置时间(教材中此段用 #if 0 屏蔽,实际由 KEY0 触发设置) */
rtc_enable();
}
void rtc_enable(void)
{
SNVS->LPCR |= (1 << 0); /* SRTC_ENV=1 使能计数器 */
while(!(SNVS->LPCR & 0x01)); /* 等待使能完成 */
}
void rtc_disable(void)
{
SNVS->LPCR &= ~(1 << 0); /* 关闭 RTC */
while(SNVS->LPCR & 0x01); /* 等待关闭完成 */
}
/* 设置日期时间:SRTC 计数器实际为 32 位 */
void rtc_setdatetime(struct rtc_datetime *datetime)
{
unsigned int seconds = rtc_coverdate_to_seconds(datetime);
unsigned int tmp = SNVS->LPCR;
rtc_disable(); /* 修改前先关闭 RTC */
SNVS->LPSRTCMR = (unsigned int)(seconds >> 17); /* 高 15 位 */
SNVS->LPSRTCLR = (unsigned int)(seconds << 15); /* 低 17 位(bit31:15) */
if(tmp & 0x1) rtc_enable(); /* 之前开着则重新打开 */
}
/* 读取当前秒数 */
unsigned int rtc_getseconds(void)
{
return (SNVS->LPSRTCMR << 17) | (SNVS->LPSRTCLR >> 15);
}
教材 bsp_rtc.c 共 9 个函数:rtc_init、rtc_enable、rtc_disable、rtc_isleapyear、rtc_coverdate_to_seconds、rtc_setdatetime、rtc_convertseconds_to_datetime、rtc_getseconds、rtc_getdatetime。时间基准为 1970-01-01,年份范围 1970~2099。
NXP参考手册中SNVS_SRTCMR和SNVS_SRTCLR的描述有误:
LCD显示实时时间,秒数持续递增。
| 要素 | 说明 |
|---|---|
| 总线 | SCL(时钟) + SDA(数据),需4.7K上拉 |
| 速率 | 标准模式100Kb/s,快速模式400Kb/s |
| 寻址 | 7位地址,支持多从机 |
| 时序 | 起始位→设备地址→ACK→数据→停止位 |
写时序: START → 设备地址+W → ACK → 寄存器地址 → ACK → 数据 → ACK → STOP
读时序: START → 设备地址+W → ACK → 寄存器地址 → ACK → 重复START → 设备地址+R → ACK → 数据 → NACK → STOP
I2C1 基地址 I2C1 = 0x021A0000。I.MX6U 使用硬件 I2C 控制器,关键寄存器:
| 寄存器 | 关键位 | 说明 |
|---|---|---|
I2Cx_IADR |
ADR(bit7:1) | I2C 从设备地址 |
I2Cx_IFDR |
IC(bit5:0) | 波特率分频(档位固定,IC=0x15 为 640 分频) |
I2Cx_I2CR |
IEN(bit7)/IIEN(bit6)/MSTA(bit5)/MTX(bit4)/TXAK(bit3)/RSTA(bit2) | 使能/中断/主从/方向/应答/重复起始 |
I2Cx_I2SR |
ICF(bit7)/IAAS(bit6)/IBB(bit5)/IAL(bit4)/SRW(bit2)/IIF(bit1)/RXAK(bit0) | 状态寄存器 |
I2Cx_I2DR |
低 8 位 | 收发数据 |
时钟源为 IPG_CLK_ROOT=66MHz;设 100KHz:IC=0x15(640 分频),66MHz/640=103.125KHz≈100KHz。
void i2c_init(I2C_Type *base)
{
base->I2CR &= ~(1 << 7); /* 要访问 I2C 寄存器,先关闭 I2C */
base->IFDR = 0x15 << 0; /* 640 分频 → ≈100KHz */
base->I2CR |= (1 << 7); /* 使能 I2C */
}
/* 发送开始信号(顺带发送从设备地址 + 读写位) */
unsigned char i2c_master_start(I2C_Type *base, unsigned char address,
enum i2c_direction direction)
{
if(base->I2SR & (1 << 5)) return 1; /* 总线忙,返回错误 */
base->I2CR |= (1 << 5) | (1 << 4); /* MSTA=1 主模式,MTX=1 发送 */
base->I2DR = ((unsigned int)address << 1) |
((direction == kI2C_Read) ? 1 : 0);
return 0;
}
i2c_master_repeated_start通过I2CR |= (1<<4)|(1<<2)(MTX=1、RSTA=1)产生重复起始。教材bsp_i2c.c共 8 个函数:i2c_init、i2c_master_start、i2c_master_repeated_start、i2c_check_and_clear_error、i2c_master_stop、i2c_master_write、i2c_master_read、i2c_master_transfer。
0x00 系统模式(写 0x04 软复位,写 0x03 开 ALS+PS+IR)、0x0A~0x0F 数据寄存器(IR/ALS/PS)。*ir = (buf[1]<<2)|(buf[0]&0x03);*als = (buf[3]<<8)|buf[2];*ps = ((buf[5]&0x3F)<<4)|(buf[4]&0x0F)。读取AP3216C的光强度、距离、红外数据,通过LCD显示。
| 要素 | 说明 |
|---|---|
| 总线 | CS(片选) + SCK(时钟) + MOSI(主出) + MISO(主入) |
| 速率 | 最高可达几十MHz,远超I2C |
| 模式 | CPOL+CPHA组合,4种模式 |
| 通信 | 全双工,收发同时进行 |
| 寻址 | 通过CS片选,无需发送设备地址 |
SPI四种模式:
| 模式 | CPOL | CPHA | 空闲电平 | 采样沿 |
|---|---|---|---|---|
| Mode 0 | 0 | 0 | 低 | 上升沿 |
| Mode 1 | 0 | 1 | 低 | 下降沿 |
| Mode 2 | 1 | 0 | 高 | 下降沿 |
| Mode 3 | 1 | 1 | 高 | 上升沿 |
ECSPI3 基地址 ECSPI3 = 0x02010000。I.MX6U 的 SPI 外设叫 ECSPI,收发各带 32×64 位 FIFO。
| 寄存器 | 关键位 | 说明 |
|---|---|---|
ECSPIx_CONREG |
BURST_LENGTH(bit31:20)/CHANNEL_SELECT(bit19:18)/DRCTL(bit17:16)/PRE_DIVIDER(bit15:12)/POST_DIVIDER(bit11:8)/CHANNEL_MODE(bit7:4)/SMC(bit3)/XCH(bit2)/EN(bit0) | 控制寄存器 |
ECSPIx_CONFIGREG |
HT_LENGTH(bit28:24)/SCLK_CTL(bit23:20)/DATA_CTL(bit19:16)/SS_POL(bit15:12)/SCLK_POL(bit7:4)/SCLK_PHA(bit3:0) | 通道极性/相位(各 4 位对应通道 3~0) |
ECSPIx_PERIODREG |
CSD_CTL(bit21:16)/CSRC(bit15)/SAMPLE_PERIOD(bit14:0) | 采样周期/片选延时 |
ECSPIx_STATREG |
TC(bit7)/RO(bit6)/RF(bit5)/RDR(bit4)/RR(bit3)/TF(bit2)/TDR(bit1)/TE(bit0) | 状态寄存器 |
ECSPIx_TXDATA / ECSPIx_RXDATA |
32 位 | 发送/接收数据 |
void spi_init(ECSPI_Type *base)
{
/* 配置 CONREG:EN=1、SMC=1、通道0主模式、突发长度 8bit */
base->CONREG = 0;
base->CONREG |= (1 << 0) | (1 << 3) | (1 << 4) | (7 << 20);
/* bit0 EN=1 使能
* bit3 SMC=1,向 TXFIFO 写数据即启动 SPI 突发
* bit7:4 CHANNEL_MODE=1,通道0 主模式(ICM-20608 接 SS0)
* bit31:20 BURST_LENGTH=7,表示 8bit */
/* 配置 CONFIGREG:通道0 PHA=0、POL=0(SPI Mode 0) */
base->CONFIGREG = 0;
/* PERIODREG:采样周期 0x2000;CSRC(bit15)=0 选择 SPI CLK 为采样时钟源 */
base->PERIODREG = 0x2000;
/* SPI CLK = (SourceCLK / (PRE_DIVIDER+1)) / 2^POST_DIVIDER
* 源时钟 pll3_sw_clk/8 = 480/8 = 60MHz;PRE_DIVIDER=0x9 → 6MHz */
base->CONREG &= ~((0xF << 12) | (0xF << 8));
base->CONREG |= (0x9 << 12);
}
/* 通道0 收发一个字节(SPI 全双工) */
unsigned char spich0_readwrite_byte(ECSPI_Type *base, unsigned char txdata)
{
uint32_t spirxdata = 0, spitxdata = txdata;
base->CONREG &= ~(3 << 18); /* 选择通道0 */
base->CONREG |= (0 << 18);
while((base->STATREG & (1 << 0)) == 0){} /* 等待 TXFIFO 空(TE=1) */
base->TXDATA = spitxdata;
while((base->STATREG & (1 << 3)) == 0){} /* 等待 RXFIFO 有数据(RR=1) */
spirxdata = base->RXDATA;
return (unsigned char)spirxdata;
}
UART2_TX_DATA(GPIO1_IO20),拉低选中;SCLK=UART2_RXD、MISO=UART2_RTS、MOSI=UART2_CTS,电气属性 0x10B1。reg &= ~0x80,读时 reg |= 0x80,低 7 位才是真实地址。0x75 WHO_AM_I:ICM-20608G=0xAF,ICM-20608D=0xAE。0x6B PWR_MGMT_1(写 0x80 复位、0x01 关睡眠)、0x1B GYRO_CONFIG、0x1C ACCEL_CONFIG、0x3B~0x48 加速度/温度/陀螺仪数据。imx6ul_hardfpu_enable() 开启 NEON 和硬件 FPU。读取ICM-20608的加速度、陀螺仪、温度数据,通过LCD显示。
本节补充教材中、上文未展开的裸机实验要点(官方SDK移植、BSP工程管理、主频时钟、GPIO中断、EPIT定时器、定时器消抖、UART、printf移植)。
MCIMX6Y2.h、fsl_common.h、fsl_iomuxc.h 与 cc.h 移植到 imx6ul/ 目录,用 imx6ul.h 统一包含。IOMUXC_SetPinMux() / IOMUXC_SetPinConfig() 和 GPIO1->GDIR 等 SDK 风格接口。cc.h 定义 __I/__O/__IO、ON/OFF 以及 uint8_t~uint64_t 等类型(第13章的 LED0 之外,ON/OFF 也来自这里)。bsp/{clk,delay,led,beep,gpio,key,exit,int,epittimer,keyfilter,uart,lcd,rtc,i2c,ap3216c,spi,icm20608}、imx6ul/、project/、obj/。INCDIRS/SRCDIRS 配合 wildcard/foreach/notdir/patsubst 自动搜集源文件,.o 统一输出到 obj/。imx6ul.lds:链接地址 0x87800000,.text 首行放 obj/start.o,__bss_start/__bss_end 标记 BSS 段。step_clk(24MHz osc) 再改 PLL1=1056MHz,最后 CCM_CACRR.ARM_PODF=2 分频得 528MHz。CCM_CCSR(STEP_SEL / PLL1_SW_CLK_SEL)、CCM_ANALOG_PLL_ARMn(DIV_SELECT)、CCM_CACRR(ARM_PODF)、CCM_CBCDR/CCM_CBCMR。GICD_* 分发器、GICC_* CPU 接口)。int_init() 初始化 GIC → GIC_EnableIRQ(IRQn) 使能中断 → system_register_irqhandler(IRQn, handler, NULL) 注册处理函数。ICR1/ICR2(触发方式) 或 EDGE_SEL(双边沿)、IMR(使能)、ISR(状态,写 1 清零);KEY0 用 GPIO1_Combined_16_31_IRQn。EPITx_CR/SR/LR/CMPR/CNR,基地址 EPIT1=0x020D0000。CR 的 CLKSRC(bit25:24)=1(ipg_clk=66MHz)、PRESCALAR(bit15:4)、RLD(bit3)=1(set-and-forget)、OCIEN(bit2)=1、ENMOD(bit1)=1;LR 设初值,CMPR=0。Tout = ((frac+1)×value)/Tclk;例程 epit1_init(0, 66000000/2) → 500ms 中断翻转 LED0。EPIT1_IRQn;SR 的 OCIF(bit0) 写 1 清零。filterkey_init/filtertimer_init/filtertimer_restart/filtertimer_stop/filtertimer_irqhandler/gpio1_16_31_irqhandler;GPIO1_IO18,filtertimer_init(66000000/100)=10ms。0x02020000,寄存器 UCR1~UCR4/UFCR/USR1/USR2/URXD/UTXD/UBIR/UBMR/UTS。CCM_CSCDR1.UART_CLK_SEL 选 pll3_80m(80MHz),UFCR.RFDIV(bit9:7)=101 为 1 分频。Baud = Ref Freq × (UBIR+1) / (16 × (UBMR+1));115200 用 UBIR=71、UBMR=3124。UCR2.SRST 写 0,轮询等待自动置 1)→ 清 UCR1 → 配 UCR2(bit14 IRTS=1、bit5 WS=1、bit2 TXEN、bit1 RXEN)→ UCR3.bit2 RXDMUXSEL=1 → 写 UFCR/UBIR/UBMR(先 UBIR 后 UBMR)→ UCR1.UARTEN=1。uart_init、uart_io_init、uart_disable、uart_enable、uart_softreset、uart_setbaudrate、putc、puts、getc、raise。stdio/{include,lib} 复制到工程根目录,Makefile 的 INCDIRS/SRCDIRS 加入 stdio/include、stdio/lib,并加编译选项 -Wa,-mimplicit-it=thumb。fputc(int ch, FILE *f) → putc(ch)、fgetc(FILE *f) → getc(),即可使用 printf/scanf。stdio 不支持浮点 %f。| 方法 | 工具 | 适用场景 |
|---|---|---|
| LED指示 | GPIO输出 | 最基础的状态指示 |
| 串口打印 | UART printf | 信息输出最常用 |
| JTAG/SWD | 调试器+IDE | 单步调试、变量查看 |
| 寄存器查看 | Memory窗口 | 验证配置是否正确 |
| 逻辑分析仪 | Saleae等 | 时序分析 |
/* 1. 寄存器操作使用位域宏 */
#define BIT(n) (1 << (n))
#define SET_BIT(reg, n) ((reg) |= BIT(n))
#define CLR_BIT(reg, n) ((reg) &= ~BIT(n))
#define GET_BIT(reg, n) (((reg) >> (n)) & 1)
/* 2. 寄存器地址使用宏定义 */
#define GPIO1_DR (*(volatile unsigned int *)0x0209C000)
/* 3. 模块化驱动结构 */
/* bsp_xxx.h → 函数声明、类型定义 */
/* bsp_xxx.c → 函数实现 */
/* main.c → 业务逻辑调用 */
/* 4. 头文件保护 */
#ifndef __BSP_XXX_H
#define __BSP_XXX_H
/* ... */
#endif
SECTIONS {
. = 0x87800000; /* 链接地址 */
.text : {
*(.text) /* 代码段 */
*(.rodata) /* 只读数据 */
}
.data : {
*(.data) /* 初始化数据 */
}
.bss : {
__bss_start = .;
*(.bss) /* 未初始化数据 */
__bss_end = .;
}
}
| 特性 | I.MX6ULL | STM32F103 | RK3568 |
|---|---|---|---|
| 内核架构 | Cortex-A7 | Cortex-M3 | Cortex-A55 |
| 主频 | 528MHz | 72MHz | 1.8GHz |
| 内存管理 | MMU(可选) | 无MMU | MMU |
| GPIO命名 | GPIO1_IO00~IO31 | PA0~15 | GPIO0_A0~D7 |
| 寄存器操作 | 直接访问 | HAL库封装 | 直接访问 |
| 启动方式 | Boot ROM→IVT→DCD | 向量表→Reset_Handler | Boot ROM→TPL→SPL |
| DDR配置 | DCD表自动初始化 | 无需(片内SRAM) | TPL/SPL初始化 |
| GPIO数量 | 124个 | 51个 | 156个 |
| 时钟配置 | CCM+PLL | RCC | CRU |
| 中断控制器 | GIC | NVIC | GIC-500 |
| 开发难度 | 中等 | 简单 | 较高 |
STM32 (HAL库):
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_SET);
I.MX6ULL (寄存器):
GPIO1->DR |= (1 << 3); /* 输出高电平 */
GPIO1->DR &= ~(1 << 3); /* 输出低电平 */
RK3568 (寄存器):
GPIO0->PORT_DR |= BIT(0); /* 输出高电平 */
GPIO0->PORT_DR &= ~BIT(0); /* 输出低电平 */
答案:
I.MX6U GPIO初始化需要4步:
关键差异:I.MX6U的GPIO复用是通过IOMUXC控制器集中管理的,而STM32是通过AFR寄存器分散管理的。
答案:
| 方法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 循环延时 | 空指令消耗CPU周期 | 实现简单 | 受编译器优化影响,精度差 |
| SysTick | Cortex-M专用24位计数器 | 精度高(1μs) | 仅Cortex-M可用 |
| EPIT | 32位定时器+中断 | 精度高,可触发中断 | 中断开销 |
| GPT | 32位定时器轮询 | 精度高(1μs),无需中断 | CPU忙等 |
I.MX6U推荐使用GPT定时器,配置ipg_clk(66MHz)为时钟源,66分频得到1MHz,计数器每加1表示1μs。
答案:
| 特性 | I2C | SPI |
|---|---|---|
| 线数 | 2根(SCL+SDA) | 4根(CS+SCK+MOSI+MISO) |
| 速率 | 最高400Kb/s | 最高几十MHz |
| 模式 | 半双工 | 全双工 |
| 寻址 | 软件发送设备地址 | 硬件CS片选 |
| 多设备 | 通过地址区分 | 通过CS片选 |
| 复杂度 | 时序复杂 | 时序简单 |
适用场景:
答案:
I.MX6U的Boot ROM在启动时会读取IVT(Image Vector Table)中的DCD(Data Configuration Data)表。DCD表存储在SD卡/eMMC的特定位置,包含了DDR控制器的所有寄存器配置和时序参数。Boot ROM读取DCD表后自动完成DDR初始化,因此用户代码中无需手动编写DDR初始化代码。
但需要注意:
答案:
I.MX6U的SNVS(Secure Non-Volatile Storage)分为两个子模块:
RTC(SRTC)位于SNVS_LP中,需要32.768KHz晶振提供时钟。即使系统主电源关闭,SNVS_LP仍由纽扣电池供电继续计时。这保证了:
汇编LED → C语言LED → 蜂鸣器 → 按键输入
↓ ↓ ↓ ↓
GPIO输出 模块化驱动 三极管驱动 GPIO输入
↓
高精度延时 ← GPT定时器
↓
DDR3 → LCD → RTC → I2C → SPI
↓ ↓ ↓ ↓ ↓
内存 显示 时间 传感器 传感器
| 阶段 | 实验 | 目标 |
|---|---|---|
| 入门 | 汇编LED → C语言LED → 蜂鸣器 → 按键 | 掌握GPIO和基础驱动 |
| 进阶 | 高精度延时 → DDR3 → LCD | 理解定时器和显示 |
| 综合 | RTC → I2C → SPI | 掌握通信协议 |
| 实战 | 串联所有实验,构建完整系统 | 集成能力 |
参考文献:
- 《I.MX6ULL参考手册》第32章 IOMUX Controller
- 《I.MX6ULL参考手册》第14章 GPT Timer
- 《I.MX6UL参考手册》第46章 SNVS
- 《I.MX6ULL参考手册》第30章 ECSPI
内容来源: 《I.MX6U嵌入式Linux驱动开发指南》第8章 汇编LED灯实验、第10章 C语言版LED灯实验、第12章 官方SDK移植实验、第13章 BSP工程管理实验、第14章 蜂鸣器实验、第15章 按键输入实验、第16章 主频和时钟配置实验、第17章 GPIO中断实验、第18章 EPIT定时器实验、第19章 定时器按键消抖实验、第20章 高精度延时实验、第21章 UART串口通信实验、第22章 串口格式化函数移植实验、第23章 DDR3实验、第24章 RGBLCD显示实验、第25章 RTC实时时钟实验、第26章 I2C实验、第27章 SPI实验