嵌入式LED控制程序详解 - 从汇编启动到C语言控制
一、整体架构概览
这个项目是一个完整的嵌入式系统程序,包含:
-
汇编启动代码(start.S):系统初始化和异常向量表
-
C语言主程序(main.c):硬件初始化和LED控制逻辑
-
Makefile:自动化编译脚本
工作流程:
上电 → 汇编启动代码 → C语言main函数 → LED闪烁
二、汇编启动代码详解(start.S)
.global _start /* 声明_start为全局符号,链接器可识别 */
_start:
/* ============================== */
/* ARM异常向量表(必须放在0x00000000) */
/* ============================== */
ldr pc, =_reset_handler /* 复位异常:系统复位或上电 */
ldr pc, =_undef_handler /* 未定义指令异常 */
ldr pc, =_software_handler /* 软件中断(SWI) */
ldr pc, =_prefect_handler /* 预取中止(指令获取失败)*/
ldr pc, =_data_abort_handler /* 数据中止(数据访问失败)*/
nop /* 保留 */
ldr pc, =_irq_handler /* 普通中断请求 */
ldr pc, =_fiq_handler /* 快速中断请求 */
/* ============================== */
/* 各个异常处理函数(简化版本) */
/* ============================== */
_undef_handler:
b _undef_handler /* 无限循环,实际应处理异常 */
_software_handler:
b _software_handler
_prefect_handler:
b _prefect_handler
_data_abort_handler:
b _data_abort_handler
_irq_handler:
b _irq_handler
_fiq_handler:
b _fiq_handler
/* ============================== */
/* 复位处理函数(主启动代码) */
/* ============================== */
_reset_handler:
/* 第一步:禁用所有中断 */
cpsid i /* 将CPSR的I位置1,禁用IRQ中断 */
/* cpsid = Change Processor State, Interrupt Disable */
/* 第二步:设置IRQ模式的栈指针 */
cps #0x12 /* 切换到IRQ模式(模式编号0x12)*/
/*
等价于传统写法:
mrs r0, cpsr ; 读取CPSR到r0
bic r0, r0, #0x1F ; 清除模式位[4:0]
orr r0, r0, #0x12 ; 设置IRQ模式(0x12)
msr cpsr, r0 ; 写回CPSR
*/
ldr sp, =0x82000000 /* 设置IRQ模式栈指针 */
/* 栈从高地址向低地址增长 */
/* 第三步:设置SYS模式的栈指针并启用中断 */
cps #0x1F /* 切换到SYS模式(0x1F)*/
/*
等价于传统写法:
mrs r0, cpsr
bic r0, r0, #0x1F ; 清除模式位
bic r0, r0, #(1 << 7) ; 清除I位(使能IRQ中断)
orr r0, r0, #0x1F ; 设置SYS模式(0x1F)
msr cpsr, r0
*/
ldr sp, =0x84000000 /* 设置SYS模式栈指针 */
cpsie i /* 使能IRQ中断 */
/* 第四步:跳转到C语言main函数 */
b main /* 程序控制权交给C代码 */
/* ============================== */
/* 程序结束处理 */
/* ============================== */
finish:
b finish /* 死循环,程序结束 */
关键概念解释:
1. ARM异常向量表
-
位置:必须放在内存地址0x00000000处
-
作用:CPU发生异常时,自动跳转到对应地址
-
8个异常向量:
-
复位(Reset)
-
未定义指令(Undefined Instruction)
-
软件中断(SWI)
-
预取中止(Prefetch Abort)
-
数据中止(Data Abort)
-
保留
-
IRQ中断
-
FIQ中断
-
2. ARM处理器模式
#define MODE_USR 0x10 // 用户模式
#define MODE_FIQ 0x11 // 快速中断模式
#define MODE_IRQ 0x12 // 普通中断模式
#define MODE_SVC 0x13 // 管理模式
#define MODE_ABT 0x17 // 中止模式
#define MODE_UND 0x1B // 未定义模式
#define MODE_SYS 0x1F // 系统模式
3. CPSR寄存器
31 30 29 28 27 26 25 24 ... 8 7 6 5 4 3 2 1 0
N Z C V - - - - ... - I F T M4 M3 M2 M1 M0
↑ ↑模式位
I位:1=禁用IRQ
三、C语言主程序详解(main.c)
1. 寄存器地址定义
/* ============================== */
/* i.MX6ULL寄存器地址定义 */
/* ============================== */
/* GPIO1_IO03引脚复用控制寄存器 */
/* 地址:0x020E0068,控制引脚功能(GPIO/UART等) */
#define IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 *((unsigned int *)0x020E0068)
/* GPIO1_IO03引脚电气特性寄存器 */
/* 地址:0x020E02F4,控制驱动能力、上下拉等 */
#define IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 *((unsigned int *)0x020E02F4)
/* GPIO1方向寄存器 */
/* 地址:0x0209C004,控制每个引脚是输入还是输出 */
#define GPIO1_GDIR *((unsigned int *)0x0209C004)
/* GPIO1数据寄存器 */
/* 地址:0x0209C000,写入数据控制引脚电平 */
#define GPIO1_DR *((unsigned int *)0x0209C000)
/* ============================== */
/* 时钟控制寄存器(CCM) */
/* ============================== */
#define CCM_CCGR0 *((unsigned int *)0x020C4068) /* 外设时钟门控0 */
#define CCM_CCGR1 *((unsigned int *)0x020C406C) /* 外设时钟门控1 */
#define CCM_CCGR2 *((unsigned int *)0x020C4070) /* 外设时钟门控2 */
#define CCM_CCGR3 *((unsigned int *)0x020C4074) /* 外设时钟门控3 */
#define CCM_CCGR4 *((unsigned int *)0x020C4078) /* 外设时钟门控4 */
#define CCM_CCGR5 *((unsigned int *)0x020C407C) /* 外设时钟门控5 */
#define CCM_CCGR6 *((unsigned int *)0x020C4080) /* 外设时钟门控6 */
2. 外设时钟使能函数
void clock_cg_init(void)
{
/* 使能所有外设时钟 */
CCM_CCGR0 = 0xFFFFFFFF; /* 全写1,使能该组所有外设时钟 */
CCM_CCGR1 = 0xFFFFFFFF;
CCM_CCGR2 = 0xFFFFFFFF;
CCM_CCGR3 = 0xFFFFFFFF;
CCM_CCGR4 = 0xFFFFFFFF;
CCM_CCGR5 = 0xFFFFFFFF;
CCM_CCGR6 = 0xFFFFFFFF;
/* 原理:CCM(Clock Controller Module)控制各个外设的时钟
1 = 时钟开启,0 = 时钟关闭
开发阶段全部开启,实际产品应只开启需要的外设 */
}
3. LED初始化函数
void led_init(void)
{
/* 第一步:配置引脚复用功能 */
/* 将GPIO1_IO03设置为GPIO功能(ALT5) */
IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = 0x05;
/*
位域说明:
0x05 = 0101b,表示ALT5模式(GPIO功能)
其他模式:
0x00 = ALT0,0x01 = ALT1,以此类推
具体功能参考芯片手册的IOMUXC章节
*/
/* 第二步:配置引脚电气特性 */
IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 = 0x10B0;
/*
位域说明(参考i.MX6ULL手册):
bit16: HYS = 0(滞后关闭)
bit15: 保留
bit14: PUS = 1(47K上拉)
bit13: PUE = 0(保持器关闭)
bit12: PKE = 1(上拉/下拉使能)
bit11: ODE = 0(开漏输出关闭)
bit10: SPEED = 2(中等速度)
bit9: 保留
bit8: DSE = 6(驱动能力R0/6)
bit7: 保留
bit6: SRE = 0(压摆率慢)
*/
/* 第三步:设置GPIO引脚为输出模式 */
GPIO1_GDIR |= (1 << 3);
/*
GPIO1_GDIR寄存器:
bit3 = 1:GPIO1_IO03为输出模式
bit3 = 0:GPIO1_IO03为输入模式
使用|=操作符只设置第3位,不影响其他位
*/
}
4. LED控制函数
/* LED点亮函数 */
void led_on(void)
{
/* 将GPIO1_IO03输出低电平(LED点亮)*/
GPIO1_DR &= ~(1 << 3);
/*
GPIO1_DR寄存器:
bit3 = 0:输出低电平,LED亮(假设LED正极接3.3V,负极接GPIO)
bit3 = 1:输出高电平,LED灭
操作分解:
1 << 3 = 00001000b (二进制)
~(1 << 3) = 11110111b (二进制)
GPIO1_DR &= 11110111b:清除第3位(设为0)
*/
}
/* LED熄灭函数 */
void led_off(void) /* 注意:原代码有拼写错误len_off(),应为led_off() */
{
/* 将GPIO1_IO03输出高电平(LED熄灭)*/
GPIO1_DR |= (1 << 3);
/*
GPIO1_DR |= 00001000b:设置第3位(设为1)
其他位保持不变
*/
}
/* 简单延时函数 */
void led_delay(unsigned int t)
{
while(t--); /* 空循环,消耗CPU时间实现延时 */
/* 注意:这不是精确延时,实际时间取决于CPU频率 */
}
5. 主函数
int main(void)
{
/* 第一步:初始化时钟(使能外设工作)*/
clock_cg_init();
/* 第二步:初始化LED引脚 */
led_init();
/* 第三步:主循环 - LED闪烁 */
while(1) /* 无限循环 */
{
led_on(); /* 点亮LED */
led_delay(0x7FFFF); /* 延时约524,287个循环 */
led_off(); /* 熄灭LED */
led_delay(0x7FFFF); /* 再次延时 */
/* 形成效果:亮→延时→灭→延时→亮... */
}
return 0; /* 实际不会执行到这里 */
}
四、Makefile详解
# ==============================================
# 编译器配置
# ==============================================
COMPILER = arm-linux-gnueabihf- # 交叉编译器前缀
CC = $(COMPILER)gcc # C编译器
LD = $(COMPILER)ld # 链接器
OBJCOPY = $(COMPILER)objcopy # 二进制转换工具
OBJDUMP = $(COMPILER)objdump # 反汇编工具
# ==============================================
# 文件列表
# ==============================================
OBJS = start.o main.o led.o # 所有目标文件
TAGRT = led # 最终目标文件名
# ==============================================
# 编译规则
# ==============================================
# 规则1:汇编文件编译规则
# 格式:目标文件 : 依赖文件
%.o : %.S # 通配符规则:所有.S文件生成.o文件
$(CC) -c -g $^ -o $@ # 编译命令
# $^ : 所有依赖文件(这里是%.S)
# $@ : 目标文件名(这里是%.o)
# -c : 只编译不链接
# -g : 包含调试信息
# 规则2:C文件编译规则
%.o:%.c # 通配符规则:所有.c文件生成.o文件
$(CC) -c -g $^ -o $@
# 规则3:链接生成最终二进制文件
$(TAGRT).bin : $(OBJS) # 依赖所有.o文件
$(LD) -Ttext 0x87800000 $^ -o $(TAGRT).elf # 链接
# -Ttext 0x87800000 : 设置代码段起始地址
# $^ : 所有.o文件
# -o : 输出文件名
$(OBJCOPY) -O binary -S -g $(TAGRT).elf $@ # 生成二进制
# -O binary : 输出二进制格式
# -S : 移除符号信息
# -g : 移除调试信息
# $@ : 目标文件(led.bin)
$(OBJDUMP) -D $(TAGRT).elf > $(TAGRT).dis # 生成反汇编
# -D : 反汇编所有段
# > : 输出重定向到文件
# ==============================================
# 清理规则
# ==============================================
clean:
rm $(OBJS) $(TAGRT).elf $(TAGRT).bin $(TAGRT).dis -rf
# 删除所有生成的文件
# -rf : 递归强制删除
# ==============================================
# 烧写规则(针对i.MX6ULL开发板)
# ==============================================
load:
./imxdownload ./$(TAGRT).bin /dev/sdb
# 使用imxdownload工具将bin文件烧写到SD卡
# /dev/sdb : SD卡设备文件(根据实际情况可能不同)
五、完整的工作流程
1. 编译流程
# 第一步:编译汇编文件
arm-linux-gnueabihf-gcc -c -g start.S -o start.o
# 第二步:编译C文件
arm-linux-gnueabihf-gcc -c -g main.c -o main.o
# 第三步:链接所有目标文件
arm-linux-gnueabihf-ld -Ttext 0x87800000 start.o main.o -o led.elf
# 第四步:生成二进制文件
arm-linux-gnueabihf-objcopy -O binary -S led.elf led.bin
# 第五步:生成反汇编文件(可选,用于调试)
arm-linux-gnueabihf-objdump -D led.elf > led.dis
2. 内存布局
内存地址 内容
0x00000000 ┌─────────────┐
│ 异常向量表 │ ← CPU上电从这里开始执行
│ (start.S) │
0x87800000 ├─────────────┤
│ 代码段 │ ← 链接地址,代码实际运行位置
│ (main.c等) │
0x82000000 ├─────────────┤
│ IRQ模式栈 │ ← 中断处理时使用
0x84000000 ├─────────────┤
│ SYS模式栈 │ ← C语言函数调用使用
└─────────────┘
3. 实际烧写和执行
# 1. 编译
make
# 2. 查看生成的文件
ls -lh led.*
# 3. 烧写到SD卡
make load
# 或者手动执行
sudo ./imxdownload ./led.bin /dev/sdb
# 4. 插入SD卡到开发板,启动
六、重要概念总结
1. 嵌入式开发核心概念
-
交叉编译:在PC上编译,在ARM开发板上运行
-
寄存器编程:直接操作硬件寄存器控制外设
-
地址映射:每个外设都有固定的内存地址
2. GPIO控制三要素
// 1. 引脚复用(选择GPIO功能)
IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = 0x05;
// 2. 电气特性配置(上下拉、驱动能力等)
IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 = 0x10B0;
// 3. 方向设置(输入/输出)
GPIO1_GDIR |= (1 << 3); // 输出模式
// 4. 数据控制(高/低电平)
GPIO1_DR &= ~(1 << 3); // 输出低电平
3. 启动代码关键步骤
-
设置异常向量表(CPU硬性要求)
-
初始化栈指针(每种模式都需要)
-
切换处理器模式
-
中断控制(先禁用,初始化后再开启)
-
跳转到C代码
4. 常见问题与调试
问题1:LED不亮
// 检查步骤:
// 1. 确认时钟使能
clock_cg_init();
// 2. 确认引脚配置正确
// - 复用功能是否正确(0x05)
// - 方向是否为输出(GPIO1_GDIR bit3=1)
// 3. 确认电平控制
// - 亮:GPIO1_DR bit3=0
// - 灭:GPIO1_DR bit3=1
// 4. 确认硬件连接
// - LED正负极是否正确
// - 是否有限流电阻
问题2:程序不运行
# 检查方法:
# 1. 查看反汇编文件
cat led.dis
# 2. 检查链接地址是否正确
# 确保-Ttext与开发板内存匹配
# 3. 检查启动代码
# 向量表是否正确
# 栈指针是否设置