进阶 IMX6ULL 裸机开发:从 C 语言点灯到 BSP 工程化(附 SDK / 链接脚本实战)

一、核心知识点铺垫: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 使用步骤
  1. 新建工程文件夹led_sdk
  2. 拷贝原有start.S/main.c/Makefile到新工程;
  3. 拷贝 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 闪烁、蜂鸣器交替响停!


总结

  1. volatile 关键字是 C 语言操作外设的必备项,防止编译器优化导致寄存器读写失效;
  2. SDK 复用无需完整安装,仅拷贝头文件即可简化寄存器定义,提升开发效率;
  3. BSP 工程结构是嵌入式开发的标准范式,将外设驱动模块化(led/beep),便于维护和扩展;
  4. 链接脚本 控制程序内存布局,启动代码需初始化.bss段为 0,否则未初始化全局变量会出错;
  5. 从汇编到 C、从零散代码到工程化,是裸机开发的核心进阶路径,后续可扩展中断、定时器等功能。
相关推荐
秋刀鱼程序编程2 小时前
Java基础入门(七)---异常处理
java·开发语言·python
遇见你的雩风2 小时前
Java---多线程(一)
java·开发语言
小白学大数据2 小时前
基于 Python 的知网文献批量采集与可视化分析
开发语言·爬虫·python·小程序
Ulyanov2 小时前
PyVista战场可视化实战(一):构建3D战场环境的基础
开发语言·python·3d·tkinter·gui开发
霸道流氓气质2 小时前
Java 实现折线图整点数据补全与标准化处理示例代码讲解
java·开发语言·windows
冬奇Lab2 小时前
【Kotlin系列10】协程原理与实战(上):结构化并发让异步编程不再是噩梦
android·开发语言·kotlin
薛不痒2 小时前
项目:矿物分类(训练模型)
开发语言·人工智能·python·学习·算法·机器学习·分类
姜太小白2 小时前
【前端】JavaScript字符串执行方法总结
开发语言·前端·javascript
被星1砸昏头2 小时前
C++与Node.js集成
开发语言·c++·算法