ARM嵌入式开发代码实践——LED灯闪烁(C语言版)

嵌入式LED控制程序详解 - 从汇编启动到C语言控制

一、整体架构概览

这个项目是一个完整的嵌入式系统程序,包含:

  1. 汇编启动代码(start.S):系统初始化和异常向量表

  2. C语言主程序(main.c):硬件初始化和LED控制逻辑

  3. 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个异常向量

    1. 复位(Reset)

    2. 未定义指令(Undefined Instruction)

    3. 软件中断(SWI)

    4. 预取中止(Prefetch Abort)

    5. 数据中止(Data Abort)

    6. 保留

    7. IRQ中断

    8. 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. 启动代码关键步骤

  1. 设置异常向量表(CPU硬性要求)

  2. 初始化栈指针(每种模式都需要)

  3. 切换处理器模式

  4. 中断控制(先禁用,初始化后再开启)

  5. 跳转到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. 检查启动代码
#    向量表是否正确
#    栈指针是否设置
相关推荐
—Qeyser2 小时前
Flutter Text 文本组件完全指南
开发语言·javascript·flutter
Aliex_git2 小时前
大模型相关概念 - LLM对话
人工智能·笔记·prompt·ai编程
咕噜企业分发小米2 小时前
豆包大模型在药物研发中的知识检索效率如何?
java·开发语言·数据库
张祥6422889042 小时前
线性代数本质十笔记
笔记·线性代数·机器学习
橘子师兄2 小时前
C++AI大模型接入SDK—快速上手
开发语言·c++·人工智能
麒qiqi2 小时前
进阶 IMX6ULL 裸机开发:从 C 语言点灯到 BSP 工程化(附 SDK / 链接脚本实战)
c语言·开发语言
Analog1112 小时前
电子秤采用 SIG5530 国产平替 CS5530
人工智能·嵌入式硬件·目标检测·硬件架构·信号处理·智能硬件
秋刀鱼程序编程2 小时前
Java基础入门(七)---异常处理
java·开发语言·python
遇见你的雩风2 小时前
Java---多线程(一)
java·开发语言