一、核心知识点铺垫:C 语言操作外设的关键
1. volatile 关键字:防止编译器 "优化" 外设寄存器
在裸机开发中,操作外设寄存器必须加volatile,否则会踩大坑!
- 作用:告诉编译器 "该变量会被硬件 / 外部因素修改,每次访问都要直接读内存,不要缓存到寄存器";
- 场景:外设寄存器(如 GPIO_DR)的数值可能被硬件自动修改,或多线程 / 中断修改,必须禁止编译器优化。
✅ 正确写法:
c
运行
// 定义GPIO1_IO03寄存器(带volatile)
#define GPIO1_DR *((volatile unsigned int *)0x0209C000)
❌ 错误写法(无 volatile):
c
运行
#define GPIO1_DR *((unsigned int *)0x0209C000)
// 编译器可能优化为:将GPIO1_DR的值缓存到寄存器,导致读写不生效
2. 寄存器地址定义的两种方式
(1)宏定义(基础版)
直接通过地址映射定义寄存器,直观但冗余:
c
运行
// CCM时钟寄存器
#define CCM_CCGR0 *((volatile unsigned int *)0x020C4068)
#define CCM_CCGR1 *((volatile unsigned int *)0x020C406C)
// GPIO1_IO03相关寄存器
#define IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 *((volatile unsigned int *)0x020E0068)
#define GPIO1_GDIR *((volatile unsigned int *)0x0209C004)
(2)结构体映射(优化版)
将寄存器按地址偏移封装为结构体,更符合 "面向硬件" 的编程习惯:
c
运行
// GPIO寄存器结构体(按IMX6ULL手册的地址偏移定义)
struct GPIO_t {
unsigned int DR; // 数据寄存器(0x00)
unsigned int GDIR; // 方向寄存器(0x04)
unsigned int PSR; // 状态寄存器(0x08)
unsigned int ICR1; // 中断配置1(0x0C)
unsigned int ICR2; // 中断配置2(0x10)
unsigned int IMR; // 中断屏蔽(0x14)
unsigned int ISR; // 中断状态(0x18)
unsigned int EDGE_SEL; // 边缘选择(0x1C)
};
// 映射GPIO1基地址
#define GPIO1 (*((volatile struct GPIO_t *)0x0209C000))
// 使用方式(更简洁)
GPIO1.GDIR |= (1 << 3); // 配置GPIO1_IO03为输出
GPIO1.DR &= ~(1 << 3); // 点亮LED
二、从纯 C 点灯到 SDK 简化开发
1. 纯 C 实现 LED 驱动(基础版)
先写核心函数,完成时钟初始化、LED 初始化、亮灭控制:
c
运行
// 时钟初始化:开启所有外设时钟
void clock_init(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) {
IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = 0x05; // 复用为GPIO
IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 = 0x10B0;// 电气特性
GPIO1_GDIR |= (1 << 3); // 输出模式
}
// 点亮LED
void led_on(void) {
GPIO1_DR &= ~(1 << 3);
}
// 熄灭LED
void led_off(void) {
GPIO1_DR |= (1 << 3);
}
// 简单延时
void led_delay(unsigned int time) {
while (time--);
}
2. 借助 NXP SDK 简化开发(进阶版)
NXP 提供的 SDK(Software Development Kit)无需完整安装,只需拷贝头文件即可复用官方封装的寄存器结构体和函数:
(1)SDK 使用步骤
- 新建工程文件夹
led_sdk; - 拷贝原有
start.S/main.c/Makefile到新工程; - 拷贝 SDK 目录下的头文件(如
fsl_common.h/fsl_iomuxc.h/MCIMX6Y2.h)到工程;
(2)SDK 版 LED 驱动(更简洁)
c
运行
// 时钟初始化(SDK封装了CCM结构体)
void clock_init(void) {
CCM->CCGR0 = 0xFFFFFFFF;
CCM->CCGR1 = 0xFFFFFFFF;
CCM->CCGR2 = 0xFFFFFFFF;
CCM->CCGR3 = 0xFFFFFFFF;
CCM->CCGR4 = 0xFFFFFFFF;
CCM->CCGR5 = 0xFFFFFFFF;
CCM->CCGR6 = 0xFFFFFFFF;
}
// LED初始化(复用SDK的引脚配置函数)
void led_init(void) {
// 配置引脚复用为GPIO1_IO03
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0);
// 配置电气特性
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0x10B0);
// 输出模式
GPIO1->GDIR |= (1 << 3);
}
// LED闪烁(异或操作)
void led_flicker(void) {
GPIO1->DR ^= (1 << 3);
}
三、工程化改造:搭建 BSP 板级支持包
裸机开发到后期,必须模块化管理代码!BSP(Board Support Package)是嵌入式开发的标准工程结构,核心是 "外设模块化、代码分层"。
1. BSP 工程目录结构
plaintext
led_bsp/
├── project/ # 核心程序(入口)
│ ├── start.S # 启动代码
│ └── main.c # 主函数
├── imx6ull/ # NXP官方头文件
│ ├── cc.h
│ ├── core_ca7.h
│ ├── fsl_common.h
│ ├── fsl_iomuxc.h
│ └── MCIMX6Y2.h
├── bsp/ # 板级支持包(外设驱动)
│ ├── led.c # LED驱动
│ ├── led.h
│ ├── beep.c # 蜂鸣器驱动
│ └── beep.h
├── Makefile # 跨目录编译脚本
└── imx6ull.lds # 链接脚本
2. 模块化实现:LED 驱动独立封装
(1)bsp/led.h(头文件)
c
运行
#ifndef __LED_H__
#define __LED_H__
void led_init(void);
void led_on(void);
void led_off(void);
void led_flicker(void);
#endif
(2)bsp/led.c(源文件)
c
运行
#include "led.h"
#include "MCIMX6Y2.h"
void led_init(void) {
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0);
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0x10B0);
GPIO1->GDIR |= (1 << 3);
led_off(); // 默认熄灭
}
void led_on(void) {
GPIO1->DR &= ~(1 << 3);
}
void led_off(void) {
GPIO1->DR |= (1 << 3);
}
void led_flicker(void) {
GPIO1->DR ^= (1 << 3);
}
3. 新增外设:蜂鸣器(BEEP)驱动
IMX6ULL 开发板的蜂鸣器由 S8550(PNP 三极管)控制,基极高电平导通:
(1)bsp/beep.h
c
运行
#ifndef __BEEP_H__
#define __BEEP_H__
void beep_init(void);
void beep_on(void);
void beep_off(void);
#endif
(2)bsp/beep.c
c
运行
#include "beep.h"
#include "MCIMX6Y2.h"
// 假设蜂鸣器接GPIO1_IO04
#define BEEP_PIN 4
void beep_init(void) {
// 配置GPIO1_IO04复用为GPIO
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO04_GPIO1_IO04, 0);
// 配置电气特性
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO04_GPIO1_IO04, 0x10B0);
// 输出模式
GPIO1->GDIR |= (1 << BEEP_PIN);
beep_off(); // 默认关闭
}
void beep_on(void) {
GPIO1->DR |= (1 << BEEP_PIN); // 高电平导通
}
void beep_off(void) {
GPIO1->DR &= ~(1 << BEEP_PIN); // 低电平关闭
}
四、工程构建优化:Makefile + 链接脚本
1. 跨目录 Makefile(支持 BSP 结构)
优化后的 Makefile 能自动编译project/和bsp/目录下的文件,更通用:
makefile
# 编译器定义
COMPILER = arm-linux-gnueabihf-
CC = $(COMPILER)gcc
LD = $(COMPILER)ld
OBJCOPY = $(COMPILER)objcopy
OBJDUMP = $(COMPILER)objdump
# 目标文件
TARGET = led_bsp
# 源文件(跨目录)
SRCS = project/start.S project/main.c bsp/led.c bsp/beep.c
# 目标文件(统一放到obj目录)
OBJS = $(patsubst %.S, obj/%.o, $(patsubst %.c, obj/%.o, $(SRCS)))
# 创建obj目录
$(shell mkdir -p obj/project obj/bsp)
# 编译规则:汇编文件
obj/%.o : %.S
$(CC) -c $^ -o $@ -g
# 编译规则:C文件
obj/%.o : %.c
$(CC) -c $^ -o $@ -g -I ./imx6ull -I ./bsp
# 链接(使用自定义链接脚本)
$(TARGET).bin : $(OBJS)
$(LD) -T imx6ull.lds $^ -o $(TARGET).elf
$(OBJCOPY) -O binary -S -g $(TARGET).elf $@
$(OBJDUMP) -D $(TARGET).elf > $(TARGET).dis
# 清理
clean:
rm -rf obj $(TARGET).elf $(TARGET).bin $(TARGET).dis
# 烧写
load:
./imxdownload $(TARGET).bin /dev/sdb
2. 链接脚本(imx6ull.lds):控制程序内存布局
链接脚本是 "连接器的蓝图",决定代码 / 数据在内存中的存放位置,还需初始化.bss段(未初始化全局变量)为 0:
ld
SECTIONS
{
/* 程序起始地址(与链接地址一致) */
. = 0x87800000;
/* 代码段:启动代码优先执行 */
.text : {
obj/project/start.o /* 启动代码放在最前面 */
*(.text) /* 其他所有代码 */
}
/* 只读数据段(如字符串常量),4字节对齐 */
.rodata ALIGN(4) : {*(.rodata*)}
/* 已初始化数据段,4字节对齐 */
.data ALIGN(4) : {*(.data)}
/* BSS段:未初始化全局变量,需要清0 */
__bss_start = .; /* BSS段起始地址 */
.bss ALIGN(4) : {
*(.bss) /* BSS段数据 */
*(.COMMON) /* 通用未初始化数据 */
}
__bss_end = .; /* BSS段结束地址 */
}
关键补充:启动代码中初始化 BSS 段
在start.S中,进入 C 语言main函数前,需将.bss段全部清 0:
armasm
/* 初始化BSS段:从__bss_start到__bss_end,全部赋值为0 */
ldr r0, =__bss_start
ldr r1, =__bss_end
mov r2, #0
bss_init:
cmp r0, r1 /* 判断是否到结束地址 */
strcc r2, [r0], #4 /* 未到则赋值0,r0+=4 */
bcc bss_init /* 循环直到BSS段初始化完成 */
/* 跳转到C语言main函数 */
bl main
/* 死循环:防止main返回后程序跑飞 */
loop:
b loop
五、主函数测试(main.c)
c
运行
#include "led.h"
#include "beep.h"
// 简单延时
void delay(unsigned int time) {
while (time--);
}
int main(void) {
// 初始化
clock_init();
led_init();
beep_init();
// 主循环:LED闪烁+蜂鸣器响停交替
while (1) {
led_on();
beep_on();
delay(0x1000000);
led_off();
beep_off();
delay(0x1000000);
}
return 0;
}
六、编译与测试
bash
运行
# 编译
make
# 清理
make clean
# 烧写到SD卡
make load
将烧写好的 SD 卡插入开发板,选择 SD 卡启动,即可看到 LED 闪烁、蜂鸣器交替响停!
总结
- volatile 关键字是 C 语言操作外设的必备项,防止编译器优化导致寄存器读写失效;
- SDK 复用无需完整安装,仅拷贝头文件即可简化寄存器定义,提升开发效率;
- BSP 工程结构是嵌入式开发的标准范式,将外设驱动模块化(led/beep),便于维护和扩展;
- 链接脚本 控制程序内存布局,启动代码需初始化
.bss段为 0,否则未初始化全局变量会出错; - 从汇编到 C、从零散代码到工程化,是裸机开发的核心进阶路径,后续可扩展中断、定时器等功能。