ARM (6) - I.MX6ULL 汇编点灯迁移至 C 语言 + SDK 移植与 BSP 工程搭建

发布于:2025-09-12 ⋅ 阅读:(21) ⋅ 点赞:(0)
回顾

一、核心关键字:volatile

1.1 作用

  • 告诉编译器:被修饰的变量会被 “意外修改”(如硬件寄存器的值可能被外设自动更新),禁止编译器对该变量进行优化(如缓存到寄存器、删除未显式修改的代码)。
  • 本质:确保每次访问变量时,都直接读取 / 写入内存地址,而非使用编译器缓存的 “旧值”。

1.2 寄存器操作中的必要性

以 GPIO 寄存器为例,若不加volatile

// 错误:编译器可能优化为“只写一次”,后续操作失效
#define GPIO1_DR *((unsigned int *)0x0209C000) 
GPIO1_DR &= ~(1<<3); // 期望拉低引脚
GPIO1_DR |= (1<<3);  // 期望拉高引脚(编译器可能认为“无用”,直接删除)

volatile后:

// 正确:每次操作都直接访问0x0209C000地址
#define GPIO1_DR *((volatile unsigned int *)0x0209C000) 

二、基础 C 语言点灯实现

2.1 寄存器地址定义

两种常见方式:直接宏定义、结构体封装(推荐后者,更易维护)。

方式 1:直接宏定义
// I.MX6ULL 关键寄存器(引脚复用、GPIO控制)
//int 指令/寄存器是四个字节 
#define IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03   *((volatile unsigned int *)0x020E0068) // 引脚复用控制
#define IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03   *((volatile unsigned int *)0x020E02F4) // 引脚电气属性(上拉/驱动能力)
#define GPIO1_DR   *((volatile unsigned int *)0x0209C000) // GPIO数据寄存器
#define GPIO1_GDIR  *((volatile unsigned int *)0x0209C004) // GPIO方向寄存器(输入/输出) 

// volatile 防止编译器优化:  reg = reg //会被优化掉
// const char *p;
// char * const p;

// ------定义时钟门寄存器地址
#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)
方式 2:结构体封装(优化访问)

按寄存器地址偏移顺序定义结构体,直接映射到基地址:

// GPIO寄存器结构体(对应I.MX6ULL GPIO模块寄存器偏移)
struct GPIO_t {
    volatile unsigned int DR;        // 数据寄存器(0x00)
    volatile unsigned int GDIR;      // 方向寄存器(0x04)
    volatile unsigned int PSR;       // 状态寄存器(0x08)
    volatile unsigned int ICR1;      // 中断控制1(0x0C)
    volatile unsigned int ICR2;      // 中断控制2(0x10)
    volatile unsigned int IMR;       // 中断屏蔽(0x14)
    volatile unsigned int ISR;       // 中断状态(0x18)
    volatile unsigned int EDGE_SEL;  // 边沿选择(0x1C)
};
// 宏定义GPIO1:结构体指针指向GPIO1基地址0x0209C000
#define GPIO1 (*((struct GPIO_t *)0x0209C000))

2.2 核心功能代码

1. 时钟初始化(必须先使能)

I.MX6ULL 外设默认时钟关闭,需打开对应时钟门控CCM_CCGRx):

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;
}

2. LED 初始化(引脚复用 + GPIO 配置)
void led_init(void) {
    // 1. 引脚复用:将GPIO1_IO03配置为GPIO功能(复用值0x05)
    IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = 0x05;
    // 2. 引脚电气属性:上拉、100MHz驱动、速度等级1(0x10B0为标准配置)
    IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 = 0x10B0;
    // 3. GPIO方向:设置GPIO1_IO03为输出(GDIR对应位写1)
    GPIO1_GDIR |= (1 << 3)
}
3. LED 控制函数

4.整体main.c

2.3  makefile 编译图表讲解

2.4 优化 Makefile(交叉编译)

针对 ARM 架构(I.MX6ULL)的交叉编译脚本,支持编译、链接、生成 bin 文件、下载:

makefile

# 交叉编译器前缀(需确保环境变量已配置)
COMPLITER = arm-linux-gnueabihf-
CC = $(COMPLITER)gcc       # 编译器
LD = $(COMPLITER)ld        # 链接器
OBJCOPY = $(COMPLITER)objcopy # 格式转换(elf→bin)
OBJDUMP = $(COMPLITER)objdump # 反汇编(elf→dis)

# 目标文件与最终目标
OBJS = start.o main.o      # 依赖的目标文件(start.S是启动汇编)
TARGET = led               # 目标名称

# 生成bin文件:依赖elf,elf依赖o文件
$(TARGET).bin : $(OBJS)
	$(LD) -Ttext 0x87800000 $^ -o $(TARGET).elf # 链接到I.MX6ULL运行地址0x87800000
	$(OBJCOPY) -O binary -S -g $(TARGET).elf $@ #  elf转bin(删除调试信息)
	$(OBJDUMP) -D $(TARGET).elf  > $(TARGET).dis # 生成反汇编文件(调试用)

# 汇编文件(.S)编译为.o
%.o : %.S
	$(CC) -c $^ -o $@ -g # -g:保留调试信息

# C文件(.c)编译为.o
%.o : %.c
	$(CC) -c $^ -o $@ -g

# 清理目标文件
clean:
	rm $(OBJS) $(TARGET).elf $(TARGET).bin $(TARGET).dis -f

# 下载到SD卡(使用imxdownload工具)
load:
	./imxdownload $(TARGET).bin /dev/sdb # /dev/sdb是SD卡设备节点

三、NXP I.MX6ULL SDK 移植

3.1 SDK 使用原则

  • SDK(Software Development Kit)包含完整 IDE(需下载器 / 仿真器,成本高),实际仅使用其头文件(标准化寄存器定义,避免硬编码)。
  • 1.SDK文件存放位置
        路径:IMAX6ULL/SDK/
        (1).SDK(Software development tools)移植 
        (2).完整开发工具就是一个IDE, 集代码编写、编译、下载于一体的集成开发环境, 类似于keil这种工具,要是用这个需要额外购买一些设备如下载器、编程器、仿真器
        (3).所以只用它的头文件
  • 关键文件:cc.hcore_ca7.hfsl_common.hfsl_iomuxc.hMCIMX6Y2.h(I.MX6ULL 芯片定义)。

3.2 移植步骤(新建工程led_sdk

  1. 工程结构初始化

    • 拷贝旧工程的start.S(启动汇编)、main.cMakefileled_sdk
    • 拷贝 SDK 所有头文件到工程根目录(或单独文件夹)。
  2. 用 SDK 重构代码(简化寄存器操作)
    SDK 头文件已封装CCMIOMUXCGPIO为结构体,直接用->访问:

    #include "MCIMX6Y2.h"   // 包含SDK芯片定义
    #include "fsl_iomuxc.h" // 包含引脚复用函数
    
    void clock_init(void) {
        // SDK已定义CCM结构体,直接访问CCGR寄存器
        CCM->CCGR0 = 0xFFFFFFFF;
        CCM->CCGR1 = 0xFFFFFFFF;
        // ... 其余CCGR寄存器同上
    }
    
    void led_init(void) {
        // 1. SDK函数:设置引脚复用(GPIO1_IO03→GPIO功能,参数2为额外配置)
        IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0);
        // 2. SDK函数:设置引脚电气属性(0x10B0为标准配置)
        IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0x10B0);
        // 3. GPIO方向配置(SDK已定义GPIO1结构体)
        GPIO1->GDIR |= (1 << 3);
    }
    

3.3 ----led_sdk------更新程序

1、查阅手册 

main.c

led.c

led.h

start.S

四、BSP(板级支持包)工程管理

4.1 工程目录结构(模块化,易维护)

    1.project :存放必要程序
        main.c start.S
    2.imx6ull :存放NXP提供的i.mx6ull头文件
        cc.h  core_ca7.h fsl_common.h fsl_iomuxc.h MCIMX6Y2.h
    3.bsp :存放硬件外设相关功能模块
        led.c led.h beep.c beep.h
    4.Makefile: 需要遍目录

led_bsp/
├── project/          # 主程序目录
│   ├── main.c        # 主函数(调用BSP模块)
│   └── start.S       # 启动汇编(初始化栈、清BSS)
├── imx6ull/          # SDK头文件目录
│   ├── cc.h
│   ├── core_ca7.h
│   ├── fsl_common.h
│   ├── fsl_iomuxc.h
│   └── MCIMX6Y2.h
├── bsp/              # 硬件外设模块目录(按外设拆分)
│   ├── led/
│   │   ├── led.c     # LED驱动实现
│   │   └── led.h     # LED驱动声明
│   └── beep/
│       ├── beep.c    # 蜂鸣器驱动实现
│       └── beep.h    # 蜂鸣器驱动声明
├── Makefile          # 多目录编译脚本(需支持遍历bsp/)
└── imx6ull.lds       # 链接脚本

4.2 BEEP 蜂鸣器驱动(程序)

  • 硬件:S8550(PNP型三极管)基极高电平导通(蜂鸣器响)。
  • 引脚:假设使用GPIO1_IO04,驱动逻辑与 LED 类似。

beep.h

#ifndef __BEEP_H
#define __BEEP_H

#include "MCIMX6Y2.h"

void beep_init(void);  // 蜂鸣器初始化
void beep_on(void);    // 蜂鸣器响
void beep_off(void);   // 蜂鸣器停

#endif

beep.c

#include "beep.h"
#include "fsl_iomuxc.h"

void beep_init(void) {
    // 1. 引脚复用:GPIO1_IO04→GPIO功能
    IOMUXC_SetPinMux(IOMUXC_GPIO1_IO04_GPIO1_IO04, 0);
    // 2. 引脚电气属性配置
    IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO04_GPIO1_IO04, 0x10B0);
    // 3. GPIO方向:输出(默认熄灭,先拉低)
    GPIO1->GDIR |= (1 << 4);
    GPIO1->DR &= ~(1 << 4);
}

void beep_on(void) {
    GPIO1->DR |= (1 << 4); // 基极高电平→PNP导通→蜂鸣器响
}

void beep_off(void) {
    GPIO1->DR &= ~(1 << 4); // 基极低电平→PNP截止→蜂鸣器停
}

main.c

makefile 改动

五、链接脚本(imx6ull.lds)

5.1 作用

  • 告诉链接器:代码段、数据段、BSS 段的存放地址和顺序(为 I.MX6ULL 启动做准备)。
  • 关键:start.S(启动代码)需放在最前面,且需初始化BSS段(未初始化全局变量清 0)。
  • 1.链接脚本: imx6ull.lds
      
  •  2.链接主要在链接阶段,为连接器提供蓝图;
      
  •  3.启动代码需要在进入C语言第一条指令前,将.bss COMMON段初始化清0

5.2 内存段总结----------链接脚本知识点

5.3 脚本内容

SECTIONS
{
    . = 0x87800000;  // 程序运行起始地址(I.MX6ULL DDR地址)

    // 代码段(.text):先放start.o(启动汇编),再放其他代码
    .text :
    {
        obj/start.o   // 启动代码优先(需确保编译时生成到obj目录)
        *(.*)         // 其他所有.text段(C代码、SDK函数等)
    } 

    // 只读数据段(.rodata):对齐4字节
    .rodata ALIGN(4) : {*(.rodata*)}

    // 已初始化数据段(.data):对齐4字节
    .data ALIGN(4) : {*(.data)}

    // BSS段(未初始化全局变量):标记起始/结束地址,供启动代码清0
    __bss_start = .;  // BSS段起始地址
    .bss ALIGN(4) : {*(.bss) *(COMMON)} // 包含BSS和COMMON段
    __bss_end = .;    // BSS段结束地址
}

5.4 关键注意点

  • 启动代码(start.S)需添加BSS段清0逻辑:
    // 清BSS段:从__bss_start到__bss_end,逐个字节写0
    ldr r0, =__bss_start
    ldr r1, =__bss_end
    mov r2, #0
    bss_loop:
        cmp r0, r1
        bge bss_end
        str r2, [r0], #4
        b bss_loop
    bss_end:
    

1.链接脚本的作用?各个段存放什么类型数据

链接脚本的核心作用
  • 告诉链接器:代码段、数据段、BSS 段的存放地址和顺序(为 I.MX6ULL 启动做准备)。
  • 关键:start.S(启动代码)需放在最前面,且需初始化BSS段(未初始化全局变量清 0)。
  • 定义程序的加载地址(如嵌入式系统中指定程序在 RAM 中的运行地址,如 i.MX6ULL 的0x87800000);
  • 规定目标文件中各个 “段(Section)” 的排列顺序和内存分配;
  • 标记特殊段(如.bss)的起始和结束地址,供启动代码初始化(如将.bss段清 0);
  • 确保代码段、数据段等按正确的内存对齐方式(如 4 字节对齐)排列,避免硬件访问错误。
各段的作用及存放数据类型

程序被编译后会拆分为多个 “段”,链接脚本通过SECTIONS命令定义这些段的位置和内容:

 
段名称 作用及存放数据类型
.text 代码段,存放可执行代码,包括:汇编指令(如start.S中的初始化代码)、C 语言函数(如mainled_init)。
.rodata 只读数据段,存放常量数据,如:字符串常量("hello")、const修饰的全局变量(const int a = 10)。
.data 初始化数据段,存放已初始化的全局变量和静态变量,如:int g_var = 5(非const且有初始值)。
.bss 未初始化数据段,存放未初始化的全局变量、或初始化为0的数据、静态变量COMMON(用于存放未初始化的非静态全局变量,如未初始化的大数组int buf[100])。
特点:程序加载时不占用磁盘空间,运行时需通过启动代码清 0(避免随机值影响)。
__bss_start/__bss_end 不是实际的段,而是链接脚本定义的标记符号,分别表示.bss段的起始和结束地址,供启动代码遍历清 0。

2.编译过程需要哪些工具,分别什么作用?

从源代码(.c.S)到可执行程序,需经过预处理→编译→汇编→链接→格式转换5 个阶段,对应工具及作用如下:

1. 预处理工具:gcc -E(预处理器)
  • 作用:处理源代码中的预处理指令(以#开头),生成纯 C 代码(.i文件)。
  • 具体操作
    • 展开#include头文件(如将#include "led.h"替换为头文件内容);
    • 替换#define宏定义(如将LED_PIN替换为实际值);
    • 删除注释、处理条件编译(#if/#else/#endif)。
  • 示例arm-linux-gnueabihf-gcc -E main.c -o main.i
2. 编译工具:gcc -S(编译器)
  • 作用:将预处理后的.i文件(纯 C 代码)转换为汇编代码(.s文件)。
  • 核心功能:进行语法检查、语义分析、代码优化(如循环展开),最终生成对应架构的汇编指令(如 ARM 架构的ldrstr指令)。
  • 示例arm-linux-gnueabihf-gcc -S main.i -o main.s
3. 汇编工具:gcc -c 或 as(汇编器)
  • 作用:将汇编代码(.s)转换为机器码(二进制目标文件,.o)。
  • 特点.o文件是 “relocatable(可重定位)” 的,即代码中的地址是相对地址(需后续链接器处理)。
  • 示例arm-linux-gnueabihf-gcc -c main.s -o main.o 或 arm-linux-gnueabihf-as main.s -o main.o
4. 链接工具:ld(链接器)
  • 作用:将多个.o目标文件(如start.omain.oled.o)合并为一个可执行文件(.elf)。
  • 核心操作
    • 解析符号引用(如main函数调用led_init时,找到led_init.text段的实际地址);
    • 按链接脚本(如imx6ull.lds)分配各段的内存地址(将相对地址转换为绝对地址);
    • 处理段的对齐和拼接。
  • 示例arm-linux-gnueabihf-ld -T imx6ull.lds start.o main.o -o led.elf
5. 格式转换工具:objcopy
  • 作用:将链接生成的.elf文件(包含符号表、调试信息等)转换为纯二进制文件(.bin),适用于嵌入式系统加载。
  • 特点.bin文件仅保留可执行代码和数据,去除调试信息,体积更小,可直接被 CPU 执行。
  • 示例arm-linux-gnueabihf-objcopy -O binary led.elf led.bin
6. 辅助工具:objdump(反汇编器)
  • 作用:将.elf文件反汇编为汇编代码(.dis),用于调试(如查看 C 代码对应的汇编指令、定位错误地址)。
  • 示例arm-linux-gnueabihf-objdump -D led.elf > led.dis

总结

  • 链接脚本是内存布局的 “规划图”,决定各段在内存中的位置和内容;
  • 编译过程是 “源代码→机器码” 的转换链,每个工具负责一个阶段,最终生成可在目标硬件上运行的二进制文件。

网站公告

今日签到

点亮在社区的每一天
去签到