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为专用GPIO |
| GPIO2 | 22个 | 0x020A0000 | 其余IO需通过IOMUXC复用 |
| GPIO3 | 29个 | 0x020A4000 | |
| GPIO4 | 29个 | 0x020A8000 | |
| GPIO5 | 12个 | 0x020AC000 | 包含SNVS域IO |
GPIO初始化三步曲:
LED0连接到GPIO1_IO03,通过三极管驱动,低电平点亮。
.global _start
_start:
@ 使能GPIO1时钟
ldr r0, =0x020C4068 @ CCM_CCGR1寄存器地址
ldr r1, [r0]
orr r1, r1, #(3 << 26) @ 设置CG14位[27:26]=11
str r1, [r0]
@ 配置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函数
C语言主函数 main.c:
#include "main.h"
int main(void)
{
/* 使能GPIO1时钟 */
unsigned int *CCM_CCGR1 = (unsigned int *)0x020C4068;
*CCM_CCGR1 |= (3 << 26);
/* 配置GPIO1_IO03复用 */
unsigned int *SW_MUX = (unsigned int *)0x020E0068;
*SW_MUX = 5;
/* 配置电气属性 */
unsigned int *SW_PAD = (unsigned int *)0x020E02F4;
*SW_PAD = 0x10B0;
/* 设置为输出 */
unsigned int *GPIO1_GDIR = (unsigned int *)0x0209C004;
*GPIO1_GDIR |= (1 << 3);
/* 输出低电平点亮LED */
unsigned int *GPIO1_DR = (unsigned int *)0x0209C000;
*GPIO1_DR &= ~(1 << 3);
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);
}
按键检测逻辑:
int main(void)
{
int value = 0;
gpio_pin_config_t key_config;
/* 初始化KEY0为输入 */
key_config.direction = kGPIO_DigitalInput;
gpio_init(GPIO1, 18, &key_config);
while(1) {
value = gpio_pinread(GPIO1, 18); /* 读取按键状态 */
if(value == 0) { /* 按下 */
delay(10); /* 消抖 */
if(gpio_pinread(GPIO1, 18) == 0) {
beep_switch(!beep_status); /* 翻转蜂鸣器状态 */
}
while(!gpio_pinread(GPIO1, 18)); /* 等待松开 */
}
}
}
按下KEY0切换蜂鸣器开关状态,实现按键控制。
GPT(General Purpose Timer)是32位向上计数器,特性:
void delay_init(void)
{
GPT1->CR = 0; /* 先关闭GPT */
GPT1->CR = (1 << 15); /* 软复位GPT */
while(GPT1->CR & (1 << 15)); /* 等待复位完成 */
GPT1->CR = (1 << 1) | (1 << 6); /* 使能+ipg_clk时钟源 */
GPT1->PR = 65; /* 66分频,1MHz */
GPT1->CR |= (1 << 0); /* 使能GPT计数器 */
}
void delay_us(unsigned int us)
{
unsigned int cnt_start = GPT1->CNT;
unsigned int cnt_end = cnt_start + us;
/* 处理溢出情况 */
if(cnt_end >= cnt_start) {
while(GPT1->CNT < cnt_end);
} else {
while(GPT1->CNT >= cnt_start); /* 等待溢出 */
while(GPT1->CNT < cnt_end);
}
}
void delay_ms(unsigned int ms)
{
unsigned int i;
for(i = 0; i < ms; i++)
delay_us(1000);
}
延时精度达到1μs级别,比循环延时准确得多。
| 延时方式 | 精度 | 原理 | 适用场景 |
|---|---|---|---|
| 循环延时 | ms级 | 编译器优化影响大 | 仅用于简单等待 |
| SysTick | μs级 | Cortex-M专用 | STM32系列 |
| GPT定时器 | 1μs | 硬件计数 | I.MX6U通用 |
| EPIT定时器 | 1μs | 单次定时+中断 | 需要中断触发 |
| 类型 | 全称 | 特点 | 代表芯片 |
|---|---|---|---|
| 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初始化:
/* DDR3初始化主要参数 */
1. 设置CCM_CSCMR1选择DDR时钟源(528MHz)
2. 配置CCM_CACRR设置DDR时钟分频
3. 设置MMDC类型寄存器(DDR3类型、位宽)
4. 配置时序参数(tRCD, tRP, tRAS等)
5. 配置校准寄存器(MPWLDECTRL, MPRDDLCTL等)
6. 使能MMDC控制器
7. 执行ZQ校准
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 | 像素时钟 |
/* LCD初始化 */
void lcd_init(void)
{
/* 1、初始化eLCDIF控制器 */
/* 2、配置LCD时序参数 */
/* 3、设置帧缓冲地址 */
/* 4、使能LCD控制器 */
}
/* 绘制像素点 */
void lcd_drawpoint(unsigned int x, unsigned int y, unsigned int color)
{
unsigned int *fb = (unsigned int *)FRAME_BUFFER_ADDR;
fb[y * LCD_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);
}
}
}
LCD屏幕显示彩色测试图案和字符信息。
I.MX6U的RTC位于SNVS(Secure Non-Volatile Storage)模块内:
void rtc_init(void)
{
/* 1、使能SNVS时钟 */
CCM->CCGR4 |= (3 << 20);
/* 2、配置SNVS_LP SRTC */
SNVS->LPCR &= ~(1 << 0); /* 先关闭SRTC */
while(SNVS->LPCR & (1 << 0)); /* 等待关闭 */
/* 3、设置初始时间(2026-09-17 12:00:00) */
rtc_settime(2026, 9, 17, 12, 0, 0);
/* 4、使能SRTC */
SNVS->LPCR |= (1 << 0);
while(!(SNVS->LPCR & (1 << 0))); /* 等待使能 */
}
void rtc_settime(int year, int month, int day, int hour, int min, int sec)
{
/* 将日期时间转换为秒数 */
unsigned int seconds = timestamp_from_ymd(year, month, day, hour, min, sec);
/* SRTC计数器是32位(非47位) */
SNVS->LPSRTCMR = (seconds >> 17) & 0x7FFF; /* 高15位 */
SNVS->LPSRTCLR = (seconds << 15) & 0xFFFF8000; /* 低17位 */
}
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
void i2c_write_reg(unsigned char addr, unsigned char reg, unsigned char data)
{
/* 1、发送起始信号 */
i2c_start();
/* 2、发送设备地址 + 写位 */
i2c_send_byte((addr << 1) | 0);
i2c_wait_ack();
/* 3、发送寄存器地址 */
i2c_send_byte(reg);
i2c_wait_ack();
/* 4、发送数据 */
i2c_send_byte(data);
i2c_wait_ack();
/* 5、发送停止信号 */
i2c_stop();
}
unsigned char i2c_read_reg(unsigned char addr, unsigned char reg)
{
unsigned char data;
i2c_start();
i2c_send_byte((addr << 1) | 0); /* 写 */
i2c_wait_ack();
i2c_send_byte(reg);
i2c_wait_ack();
i2c_start(); /* 重复起始 */
i2c_send_byte((addr << 1) | 1); /* 读 */
i2c_wait_ack();
data = i2c_read_byte(0); /* NACK + STOP */
i2c_stop();
return data;
}
读取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 | 高 | 上升沿 |
void spi_init(void)
{
/* 1、配置ECSPI3为主模式 */
ECSPI3->CONREG = 0; /* 先关闭 */
ECSPI3->CONREG |= (7 << 24); /* 突发长度=8bit */
ECSPI3->CONREG |= (0 << 18); /* 通道0 */
ECSPI3->CONREG |= (0 << 16); /* 不关心SPI_RDY */
ECSPI3->CONREG |= (0 << 12); /* 预分频=1 */
ECSPI3->CONREG |= (0 << 8); /* 分频=1 */
ECSPI3->CONREG |= (1 << 4); /* 通道0主模式 */
ECSPI3->CONREG |= (0 << 3); /* SMC=0 */
ECSPI3->CONREG |= (1 << 2); /* XCH=1,开始突发 */
ECSPI3->CONREG |= (1 << 0); /* 使能SPI */
/* 2、配置SPI模式(CPOL=1, CPHA=1 → Mode 3) */
ECSPI3->CONFIGREG = 0;
ECSPI3->CONFIGREG |= (3 << 4); /* 模式3 */
}
unsigned char spi_transfer(unsigned char data)
{
unsigned int dummy;
while(!(ECSPI3->STATUSREG & (1 << 0))); /* 等待TXFIFO空 */
ECSPI3->TXDATA = data; /* 发送数据 */
while(!(ECSPI3->STATUSREG & (1 << 3))); /* 等待RXFIFO满 */
dummy = ECSPI3->RXDATA; /* 读取数据 */
return (unsigned char)dummy;
}
读取ICM-20608的加速度、陀螺仪、温度数据,通过LCD显示。
| 方法 | 工具 | 适用场景 |
|---|---|---|
| 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