回顧
一、核心關鍵字: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.h?? ? ? ? ? ? ? ? ? ? ? 時鐘相關定義;
core_ca7.h? ? ? ? ? ? ? ? ? ?ARM Cortex-A7 內核相關定義;
fsl_common.h? ? ? ? ? ? ? ? ?通用工具函數定義;
fsl_iomuxc.h? ? ? ? ? ? ? ? ?引l腳復用配置函數定義;
MCIMX6Y2.h? ? ? ? ? ? ? ? ? ?I.MX6ULL 寄存器映射結構體定義
3.2 移植步驟(新建工程led_sdk
)
工程結構初始化:
- 拷貝舊工程的
start.S
(啟動匯編)、main.c
、Makefile
到led_sdk
。 - 拷貝 SDK 所有頭文件到工程根目錄(或單獨文件夾)。
- 拷貝舊工程的
用 SDK 重構代碼(簡化寄存器操作):
SDK 頭文件已封裝CCM
、IOMUXC
、GPIO
為結構體,直接用->
訪問:#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
--------------------------------------------------------------
實現io復用功能配置 -- sdk fsl_iomuxc.h?
實現電氣特性配置 -- 對應sdk fsl_iomuxc.h 部分
配置方向寄存器 -- 對應sdk MCIMX6Y2.h 部分
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 第一次改動
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, r1bge bss_endstr r2, [r0], #4b 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 語言函數(如main
、led_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 架構的
ldr
、str
指令)。- 示例:
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.o
、main.o
、led.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
總結
- 鏈接腳本是內存布局的 “規劃圖”,決定各段在內存中的位置和內容;
- 編譯過程是 “源代碼→機器碼” 的轉換鏈,每個工具負責一個階段,最終生成可在目標硬件上運行的二進制文件。