1. 项目概述
在嵌入式开发中,LED灯控制是最基础也是最经典的入门实验。通过这个实验,我们可以学习如何从零开始搭建一个裸机程序,理解处理器启动流程,掌握C语言与汇编的混合编程技巧。本次实验基于I.MX6ULL处理器,使用C语言实现LED灯的闪烁控制,相比纯汇编版本更贴近实际工程开发。
提示:裸机程序是指直接在硬件上运行,不依赖任何操作系统的程序。它需要开发者手动管理所有硬件资源和程序流程。
2. 硬件原理分析
2.1 LED电路设计
实验板上的LED电路采用典型的共阳极设计:
- LED阳极通过限流电阻连接到3.3V电源
- LED阴极连接到处理器的GPIO引脚
- 当GPIO输出低电平时,LED导通发光
- 当GPIO输出高电平时,LED截止熄灭
具体到I.MX6ULL开发板:
- 使用的LED连接在GPIO1_IO03引脚
- 限流电阻值为1kΩ
- 工作电压3.3V
2.2 寄存器映射
控制LED需要配置以下三类寄存器:
-
时钟控制模块(CCM)寄存器:
- 用于使能GPIO模块的时钟
- 地址范围:0x020C4000~0x020C4080
- 本实验需要配置CCGR0~CCGR6寄存器
-
IOMUX控制器寄存器:
- 用于配置引脚复用功能
- 地址范围:0x020E0000~0x020E9FFF
- 需要配置SW_MUX_GPIO1_IO03和SW_PAD_GPIO1_IO03
-
GPIO控制器寄存器:
- 用于控制GPIO输入输出
- GPIO1基地址:0x0209C000
- 主要使用GDIR(方向寄存器)和DR(数据寄存器)
3. 实验程序编写
3.1 工程结构设计
创建工程目录ledc,包含以下文件:
code复制ledc/
├── start.S # 汇编启动文件
├── main.c # C语言主程序
├── main.h # 寄存器定义头文件
├── Makefile # 构建脚本
└── imx6ul.lds # 链接脚本
3.2 汇编启动代码(start.S)
assembly复制.global _start /* 全局标号 */
/*
* 描述:_start函数,程序从此函数开始执行
*/
_start:
/* 进入SVC模式 */
mrs r0, cpsr /* 读取CPSR到r0 */
bic r0, r0, #0x1f /* 清除模式位M[4:0] */
orr r0, r0, #0x13 /* 设置为SVC模式 */
msr cpsr, r0 /* 写回CPSR */
ldr sp, =0X80200000 /* 设置栈指针 */
b main /* 跳转到main函数 */
关键点解析:
-
处理器模式切换:
- 复位后处理器处于Supervisor(SVC)模式
- 通过修改CPSR寄存器的M[4:0]位设置模式
- SVC模式是操作系统内核常用模式,具有完全硬件访问权限
-
栈指针设置:
- C语言函数调用需要栈空间
- 0x80200000位于DDR3内存区域
- 分配2MB栈空间足够裸机程序使用
-
跳转机制:
- 使用b指令跳转到main函数
- 不保存返回地址,因为裸机程序不需要返回
3.3 C语言主程序(main.c)
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;
}
/* LED初始化 */
void led_init(void)
{
/* 1. 初始化IO复用 */
SW_MUX_GPIO1_IO03 = 0x5; /* 复用为GPIO1_IO03 */
/* 2. 配置GPIO1_IO03电气属性 */
SW_PAD_GPIO1_IO03 = 0X10B0;
/* 3. 初始化GPIO方向 */
GPIO1_GDIR = 0X0000008; /* GPIO1_IO03设置为输出 */
/* 4. 默认输出低电平,LED亮 */
GPIO1_DR = 0X0;
}
/* LED控制函数 */
void led_on(void) { GPIO1_DR &= ~(1<<3); }
void led_off(void) { GPIO1_DR |= (1<<3); }
/* 延时函数 */
void delay_short(volatile unsigned int n) { while(n--){} }
void delay(volatile unsigned int n) { while(n--) delay_short(0x7ff); }
/* 主函数 */
int main(void)
{
clk_enable(); /* 使能时钟 */
led_init(); /* 初始化LED */
while(1) {
led_off();
delay(500);
led_on();
delay(500);
}
return 0;
}
3.4 寄存器定义(main.h)
c复制/* CCM相关寄存器 */
#define CCM_CCGR0 *((volatile unsigned int *)0X020C4068)
#define CCM_CCGR1 *((volatile unsigned int *)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)
/* IOMUX相关寄存器 */
#define SW_MUX_GPIO1_IO03 *((volatile unsigned int *)0X020E0068)
#define SW_PAD_GPIO1_IO03 *((volatile unsigned int *)0X020E02F4)
/* GPIO1相关寄存器 */
#define GPIO1_DR *((volatile unsigned int *)0X0209C000)
#define GPIO1_GDIR *((volatile unsigned int *)0X0209C004)
4. 编译与链接
4.1 Makefile编写
makefile复制objs := start.o main.o
ledc.bin: $(objs)
arm-linux-gnueabihf-ld -Timx6ul.lds -o ledc.elf $^
arm-linux-gnueabihf-objcopy -O binary -S ledc.elf $@
arm-linux-gnueabihf-objdump -D -m arm ledc.elf > ledc.dis
%.o: %.s
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $<
%.o: %.S
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $<
%.o: %.c
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $<
clean:
rm -rf *.o ledc.bin ledc.elf ledc.dis
关键选项说明:
-nostdlib:不使用标准库,裸机程序需要-O2:优化等级2,平衡代码大小和性能-Timx6ul.lds:指定链接脚本
4.2 链接脚本(imx6ul.lds)
ld复制SECTIONS{
. = 0X87800000;
.text : {
start.o
main.o
*(.text)
}
.rodata ALIGN(4) : {*(.rodata*)}
.data ALIGN(4) : {*(.data)}
__bss_start = .;
.bss ALIGN(4) : {*(.bss) *(COMMON)}
__bss_end = .;
}
链接地址说明:
- 0x87800000是I.MX6ULL的DDR3起始地址
- 程序将被加载到此地址运行
- 前1MB空间保留给uboot使用
5. 下载与调试
5.1 烧写工具准备
使用imxdownload工具将bin文件烧写到SD卡:
bash复制chmod 777 imxdownload # 添加执行权限
./imxdownload ledc.bin /dev/sdb # 烧写到SD卡
5.2 常见问题排查
-
LED不亮:
- 检查硬件连接是否正确
- 确认GPIO引脚配置正确
- 使用示波器测量GPIO输出电平
-
程序不运行:
- 确认烧写地址正确(0x87800000)
- 检查链接脚本是否正确
- 查看反汇编文件ledc.dis确认代码布局
-
延时不准:
- 调整delay_short参数
- 考虑使用定时器实现精确延时
6. 进阶思考
6.1 启动流程优化
完整的启动流程还应包括:
- 关闭看门狗
- 初始化时钟
- 初始化DDR
- 设置向量表
- 清除BSS段
6.2 代码结构改进
可以将代码模块化:
- 单独时钟初始化模块
- GPIO驱动模块
- 延时模块
- 主应用模块
6.3 功能扩展
基于此框架可以扩展:
- 按键控制LED
- PWM调光
- 串口控制
- 中断处理
经验分享:在实际项目中,建议使用寄存器定义头文件来自芯片厂商,而不是手动定义。这样可以避免地址错误,也便于后续维护。
