ARM的一些常见问题
- ARM 体系结构的主要特点是什么?
- 精简指令集 (RISC):ARM 采用 RISC 结构,指令集较小且简单,执行效率高。相比于复杂指令集 (CISC),RISC 更强调每条指令的执行速度。
- 低功耗设计:ARM 处理器通过简化指令集和减少每条指令的处理步骤,达到了低功耗的效果,适用于移动设备和嵌入式系统。
- Thumb 和 Thumb-2 指令集:ARM 支持 16 位 Thumb 指令集,它能够提高代码密度,特别适合对内存有限的嵌入式系统。
- 多核和对称多处理 (SMP):现代 ARM 处理器支持多核架构,并且多核处理器可以并行处理多个任务。
- 向量浮点单元 (VFP) 和 NEON:ARM 处理器通常集成 VFP(向量浮点运算单元)和 NEON 技术来加速多媒体和信号处理任务。
- ARM 的中断处理机制是怎样的?
ARM 的中断机制是通过中断向量表来处理的,不同的中断类型有不同的优先级。主要的中断处理流程如下:
- 中断请求 (IRQ):常规外设或定时器产生的中断请求。
- 快速中断 (FIQ):高优先级的中断请求,常用于紧急任务处理。FIQ 有独立的寄存器组来加快中断响应速度。
- 中断向量表:中断向量表存储在一个固定的地址(如地址 0x00000000 或 0xFFFF0000),每种中断类型有一个固定的入口地址,处理器通过跳转到相应的中断向量表位置来响应中断。
- 嵌套中断:在 ARM 架构中,通过管理中断屏蔽和优先级,可以支持嵌套中断,即在处理中断的过程中可以响应更高优先级的中断。
- ARM Cortex-M 系列的主要特点是什么?
ARM Cortex-M 系列是专为嵌入式系统设计的,主要特点包括:
- 基于 ARMv7-M 或 ARMv8-M 指令集。
- 低功耗,非常适合电池供电的嵌入式设备。
- 支持快速中断响应,通过 NVIC(嵌套向量中断控制器)实现高效的中断管理,最多支持 240 个中断通道。
- 内置调试支持:如 DWT(数据观察点和跟踪)和 ITM(指令跟踪宏单元)。
- 浮点运算单元 (FPU):Cortex-M4 和 Cortex-M7 支持硬件浮点运算,适合需要大量数值计算的应用。
- ARM 处理器的总线接口有哪些?
ARM 处理器主要使用以下总线接口技术:
- AMBA (Advanced Microcontroller Bus Architecture):AMBA 是 ARM 提出的总线协议,常用的总线类型包括:
- AHB (Advanced High-performance Bus):主要用于高性能外围设备和高速数据传输。
- APB (Advanced Peripheral Bus):用于低速外围设备,如 GPIO、串口等。
- AXI (Advanced eXtensible Interface):用于高性能内存和外设之间的数据传输,支持并行和流水线操作。
- 如何在 ARM 体系结构中实现内存映射I/O?
内存映射I/O 是 ARM 处理器中外设与内存地址空间共享的一种机制,具体步骤如下:
- 内存映射:每个外设都分配有固定的内存地址,处理器可以通过读写这些内存地址来控制外设。
- 地址分配:不同外设的地址范围通常在芯片手册中指定。例如,GPIO 的控制寄存器可能映射到一个特定的地址段。
- 访问外设:通过普通的 ldr 和 str 指令,ARM 处理器可以访问这些内存映射的外设。
- ARM中的异常处理机制是什么?
ARM 处理器的异常处理机制与中断类似,但处理的是软件错误或其他需要特殊处理的事件。常见的异常包括:
- 复位异常 (Reset):设备复位时触发。
- 未定义指令异常 (Undefined Instruction):执行未定义的指令时触发。
- 数据预取异常 (Data Abort/Prefetch Abort):发生内存访问错误或非法地址访问时触发。
- 软件中断 (SWI):用于用户态调用内核服务(类似于系统调用)。
- ARM 的虚拟内存管理是如何实现的?
ARM 处理器支持虚拟内存管理,通过以下方式实现:
- MMU (Memory Management Unit):用于将虚拟地址转换为物理地址,并提供访问控制。
- 分页机制:ARM 使用分页来管理内存,将内存划分为固定大小的页,虚拟地址通过页表映射到物理地- 址。
- TLB (Translation Lookaside Buffer):TLB 用于缓存最近使用的页表项,减少内存访问时的转换开销。
安装交叉编译工具
- 拷贝 gcc-4.6.4.tar.xz 到 Linux 环境,并解压
tar -xvf gcc-4.6.4.tar.xz
- 将整个 GCC 目录安装到系统
- 方法:将 gcc-4.6.4/bin 目录添加到 PATH 环境变量
- 修改 ~/.bashrc 文件,添加以下内容,在文件底部添加
export PATH=$PATH:/home/lsf/source/gcc-4.6.4/bin
- source ~/.bashrc 使修改立即生效
- 测试安装
终端中输入 arm-none- 并使用 Tab 键自动补齐,检查工具是否可用 - 注意
- 若是 64 位系统,可能需要安装 32 位支持库
sudo apt-get install lib32ncurses5 lib32z1
接口技术
- CPU: 狭义上指的是 核, 广义上指的是 整个芯片
- 狭义上的 CPU:指处理器的 核心 (core) 部分,负责执行指令、处理数据和控制计算过程。它包括了运算逻辑单元 (ALU)、寄存器、控制单元等,核心的任务是执行软件的指令集。
- 广义上的 CPU:可以包括整个芯片,除了核心以外还包括缓存 (cache)、总线控制器、内存管理单元 (MMU) 等部件。它不仅仅是执行指令的逻辑单元,还包括一些周边的控制逻辑和接口。
- SOC : system on chip 片上系统, 核+接口
- SoC 可以看作是一个集成了 CPU、接口以及其他外设的单芯片系统,适合于嵌入式系统和移动设备应用
- 裸机编程: 没有操作系统的编程,比如 单片机
- STM32:基于 ARM Cortex-M 核心,广泛应用于工业控制和消费电子。
- 51 单片机:经典的8位单片机,适合学习与小型应用。
裸机编程示例图
- 核心流程:
ldr 指令:CPU 使用 ldr(load register)指令,从存储器或外设的寄存器读取数据到寄存器。
str 指令:CPU 使用 str(store register)指令,将寄存器的数据写回存储器或外设的寄存器中。
- 硬件,分为两部分 核心板+外设板
- 核心板:能让CPU正常运行的最小系统,主要组件:CPU,flash,RAM,电源,串口,晶振
- 外设板: 外设板通常包含了许多外部设备,这些设备通过各种接口与核心板的 CPU 连接,用于扩展系统的功能。例如:扬声器、SD卡接口、ADC、HDMI
- 核心板和外设板通过接口(如 GPIO、I2C、SPI、UART 等)进行连接和通信。
操作硬件一般步骤(以 LED 为例)
- 从电路板上找到该硬件
- 找到你想控制的硬件设备。例如:LED2
- 记下该设备的标号(如 LED2),方便后续在电路图和芯片手册中查找相关信息
- 打开电路图,找到该硬件的连接信息
- 打开硬件的 电路原理图,找到 LED2 相关的部分
- 在电路图中查找 LED2 的位置,并查看该 LED 是如何连接到 CPU 的管脚上。例如,你可能会发现 LED2 连接到 CPU 的 GPX2_7 引脚
- 阅读芯片手册 (datasheet),找到相关控制寄存器
- 打开对应的 芯片手册 (datasheet),找到涉及控制 GPX2_7 引脚的章节,通常是在 GPIO(通用输入输出)控制器章节中
- GPX2_7 表示 GPIO X 组的第 7 个引脚,你需要找到该引脚的寄存器配置方式
寄存器的相关配置: GPX2CON(引脚配置寄存器):配置该引脚的模式(输入/输出)。 寄存器地址:0x11000C40。 操作说明:设置 [31:28] 位为 0001,表示将 GPX2_7 设置为输出模式。 GPX2DAT(数据寄存器):用于控制引脚的高低电平(控制开/关)。 寄存器地址:0x11000C44。 操作说明:[7] 位控制 GPX2_7 的输出状态。设置该位为 1 则为高电平(LED 点亮),设置为 0 则为低电平(LED 熄灭)。
- 编写代码,进行编程和测试
- 代码在start.s中写,通过Makefile文件编译,取bin文件烧录进去开发板即可
.text
@@@ 通过 ARM 裸机编程直接控制硬件,具体任务是通过按键控制 LED 的亮灭,即当按键按下时点亮 LED,松开按键时熄灭 LED。
@@ 配置 GPX2_7 为输出模式
ldr r0,=0x11000c40 @ 加载 GPX2CON 地址
ldr r2,[r0] @ 读取 GPX2CON 内容到 r2
bic r2,r2,#0xF0000000 @ 清除 GPX2_7 的控制位
orr r2,r2,#0x10000000 @ 设置 GPX2_7 为输出模式 (0001)
str r2,[r0] @ 将修改后的值写回 GPX2CON
@@ 配置 K2 为输入模式 (GPX1_1)
ldr r0,=0x11000c20 @ 加载 GPX1CON 地址
ldr r2,[r0] @ 读取 GPX1CON 内容到 r2
bic r2,r2,#0xF0 @ 清除 GPX1_1 的控制位
str r2,[r0] @ 直接清除后默认是输入模式 (0000)
loop:
ldr r0,=0x11000c24 @ 加载 GPX1DAT 地址 (检测按键输入)
ldr r2,[r0] @ 读取 GPX1DAT
and r2,r2,#2 @ 检查 GPX1_1 状态,检查按键是否按下
cmp r2,#0
bleq light_led_fun @ 如果按下按键,调用点亮 LED 函数
blne off_led_fun @ 如果没有按下,调用熄灭 LED 函数
bl sdelay @ 延时
b loop @ 继续循环
@@ 点亮 LED
light_led_fun:
ldr r0,=0x11000c44 @ 加载 GPX2DAT 地址
ldr r2,[r0] @ 读取 GPX2DAT
orr r2,r2,#0x80 @ 设置 GPX2_7 为高电平,点亮 LED
str r2,[r0] @ 写回 GPX2DAT
mov pc,lr @ 返回
@@ 熄灭 LED
off_led_fun:
ldr r0,=0x11000c44 @ 加载 GPX2DAT 地址
ldr r2,[r0] @ 读取 GPX2DAT
bic r2,r2,#0x80 @ 清除 GPX2_7 高电平,熄灭 LED
str r2,[r0] @ 写回 GPX2DAT
mov pc,lr @ 返回
@@ 延时函数
sdelay:
ldr r10,=0xFFFF @ 加载一个较大的值,用于延时
delay_loop:
sub r10,r10,#1 @ 自减
cmp r10,#0 @ 检查是否达到 0
bgt delay_loop @ 如果没有到 0,继续延时
mov pc,lr @ 返回
.end
GPIO章节
- 概述
- GPIO(General Purpose Input/Output,通用输入输出端口) 是嵌入式系统中用于控制和管理芯片外部管脚的模块。
- GPIO 模块允许用户根据需要将芯片的管脚配置为输入或输出,并通过这些管脚与外部设备进行交互(例如,控制 LED、读取传感器信号等)。
- 多功能引脚:GPIO 引脚可以配置为多个功能,不仅限于通用输入输出,还可以配置为串口、USB、I2C、SPI 等特定功能。
- GPIO 管脚的分组管理
- 为了有效管理芯片上众多管脚,GPIO 管脚通常按组分类,不同组的 GPIO 管脚通过寄存器进行独立控制。例如:
GPA:组 A 管脚,如 GPA_0, GPA_1。
GPB:组 B 管脚。
GPX1, GPX2:组 X 的第 1 和第 2 组管脚。
GPD:组 D 管脚,依次类推。 - 每组 GPIO 通常包含 16 个引脚,可以独立配置为输入或输出,每组都有自己的控制寄存器。
- GPIO 的基本功能
- 输出功能
- 输入功能
- GPIO的其他功能
- 串口(UART):用于串行通信。
- USB 接口:用于外设通信。
- I2C/SPI 接口:用于与传感器或其他外部设备的同步通信。
- 中断引脚:部分 GPIO 可以配置为中断引脚,当管脚电平变化时触发中断信号,通知处理器做出响应。
- 使用示例在资源文件里面,开发板为fs4412
串口通信
串口(Serial)是一种常见的通信方式,它通过串行传输数据,即一次一位地发送或接收数据。相比之下,并口(Parallel)一次可以发送多个数据位,但由于并口需要更多的引脚,所以在嵌入式系统中较少使用。
基本概念
- 串口控制器:负责管理串口通信的硬件模块,控制数据的发送与接收。
- 串行通信:数据按位顺序依次传输,通常用于设备之间的数据交换,常见于嵌入式系统、传感器等应用。
- 并行通信:数据一次性发送多位数据,虽然速度快,但需要更多的引脚和布线,较少在现代嵌入式系统中使用。
缺点
- 速度较慢:相比并行通信,串行通信速度较慢,因为它一次只能传输一位数据。
- 数据量较小:串口通信一次发送的数据量较小,通常是字节级别的数据。
- 误码率问题:由于信号在传输过程中可能受到噪声、干扰等影响,导致数据误码。
- 同步问题:串行通信需要发送方和接收方的时钟保持同步,如果不同步,可能会导致数据丢失。
串口通信代码编写流程
- 找到板子上的串口硬件
- 在电路板上找到串口接口硬件,例如 COM2 或 CON7,这些端口通常连接到处理器的串行通信接口上,用于发送和接收数据
- 在电路图中查找对应的 GPIO 引脚
- 根据硬件端口的编号(如 COM2/CON7),打开电路原理图,查找该端口连接到芯片的哪些 GPIO 管脚。
例如:在电路图中查找到 COM2 连接到芯片的 GPA1_0 和 GPA1_1 引脚。
- 根据硬件端口的编号(如 COM2/CON7),打开电路原理图,查找该端口连接到芯片的哪些 GPIO 管脚。
- 将 GPIO 配置为串口模式
- 打开芯片的 datasheet(数据手册),查找 GPIO 章节,找到对应的管脚配置寄存器,并将这些 GPIO 管脚配置为串口模式。
- 例如:对于 GPA1_0 和 GPA1_1,通过配置寄存器将其设置为串口的 TX(发送)和 RX(接收)引脚。
GPA1CON = 0x11400020; // GPA1CON 寄存器地址
GPA1CON &= ~(0xFF); // 清除 GPA1_0 和 GPA1_1 的原有配置
GPA1CON |= (0x22); // 配置 GPA1_0 和 GPA1_1 为串口模式 (0x2)
- 配置串口寄存器
- 在 UART 串口章节 中,找到串口控制器的相关寄存器,配置波特率、停止位、数据位等参数。
- 配置串口寄存器的步骤
- ULCON2:配置数据宽度、停止位、校验方式。
- UCON2:配置接收和发送的模式(轮询模式或中断模式)。
- UTRSTAT2:用于检测发送和接收状态,判断是否可以继续发送或接收数据。
- UTXH2 和 URXH2:发送和接收数据的寄存器。
- UBRDIV2 和 UFRACVAL2:配置波特率。
- 波特率计算
- 根据波特率和系统时钟(SCLK_UART),需要配置波特率寄存器 UBRDIV2 和 UFRACVAL2。
- 波特率公式
D I V _ V A L = ( S C L K _ U A R T / ( 波特率 × 16 ) ) – 1 DIV\_VAL = (SCLK\_UART / (波特率 × 16)) – 1 DIV_VAL=(SCLK_UART/(波特率×16))–1
例如:波特率为 115200 bps,SCLK_UART 为 100 MHz
DIV_VAL = (100000000 / (115200 × 16)) - 1 = 54.253 - 1 = 53.253
UBRDIVn = 53 // 整数部分
UFRACVALn = 4 // 小数部分(0.253 × 16 = 4)
串口与PC通信代码的编写
- 编写main.c
//嵌入式系统中发送 "hello world" 字符串
/*
## GPIO章节 中将管脚配置为串口模式
GPA1CON 0x11400020 [3:0]=0x2 [7:4]=0x2
## UART串口章节, 配置寄存器 波特率 停止位 数据值 校验位 .....
ULCON2 0x13820000 [1:0] = 0x3 8bit data width
[2] = 0 1 stop bit
[5:3]=000 no parity
[6] = 0 no IR
UCON2 0x13820004 [1:0]=0x01 poll for recv
[3:2]=0x01 poll for send
UTRSTAT2 0x13820010 x=[0] 1,接收buf有了数据 0-nodata
x=[1] ,1 可以继续发送了 0-数据还在发送中
UTXH2 0x13820020 [7:0]=data;
URXH2 0x13820024 data=[7:0];
UBRDIV2 0x13820028 53
UFRACVAL2 0x1382002C 4
*/
// GPIO 和 UART 寄存器的地址定义
#define GPA1CON *(volatile long*)0x11400020 // GPA1CON GPIO 引脚控制寄存器
#define ULCON2 *(volatile long*)0x13820000 // ULCON2 串口控制寄存器,设置数据宽度、校验位等
#define UCON2 *(volatile long*)0x13820004 // UCON2 串口控制寄存器,设置发送和接收模式
#define UTRSTAT2 *(volatile long*)0x13820010 // UTRSTAT2 串口状态寄存器,检查是否可以发送和接收
#define UTXH2 *(volatile long*)0x13820020 // UTXH2 串口发送数据寄存器
#define URXH2 *(volatile long*)0x13820024 // URXH2 串口接收数据寄存器
#define UBRDIV2 *(volatile long*)0x13820028 // 波特率整数部分寄存器
#define UFRACVAL2 *(volatile long*)0x1382002C // 波特率小数部分寄存器
// 初始化 UART 串口
void uart_init(void)
{
// 配置 GPA1_0 和 GPA1_1 为串口模式
GPA1CON = GPA1CON & ~0xF; // 清除 GPA1_0 的原有配置
GPA1CON = GPA1CON | (1<<1); // 设置 GPA1_0 为 UART TX (串口发送)
GPA1CON = GPA1CON & ~(0xF<<4); // 清除 GPA1_1 的原有配置
GPA1CON = GPA1CON | (1<<5); // 设置 GPA1_1 为 UART RX (串口接收)
// 配置 ULCON2,设置为 8 位数据宽度、1 位停止位,无校验
ULCON2 = ULCON2 | 0x3; // 设置数据宽度为 8 位
ULCON2 = ULCON2 & ~(1<<2); // 设置 1 个停止位
ULCON2 = ULCON2 & ~(7<<3); // 禁用校验位 (no parity)
ULCON2 = ULCON2 & ~(1<<6); // 禁用红外模式 (no IR)
// 配置 UCON2,设置为轮询模式
UCON2 &= ~(3); // 清除原有配置
UCON2 |= 1<<0; // 设置为轮询接收模式
UCON2 &= ~(3<<2); // 清除原有发送配置
UCON2 |= 1<<2; // 设置为轮询发送模式
// 设置波特率为 115200
UBRDIV2 = 53; // 设置波特率的整数部分
UFRACVAL2 = 4; // 设置波特率的小数部分
}
// 发送单个字符
void putc(char ch)
{
// 等待直到发送缓冲区为空
while( ( UTRSTAT2 & (1<<1) ) == 0 );
// 向发送寄存器中写入数据
UTXH2 = ch;
}
// 发送字符串
void puts(char *s)
{
int i = 0;
// 循环发送字符串中的每个字符
while(s[i]) {
putc(s[i]); // 发送单个字符
i++;
}
}
// 简单的延时函数,延时 ms 毫秒
void mysleep(int ms)
{
while(ms--) {
int num = 0x1FFF/2; // 简单的延时循环
while(num--);
}
}
// 主函数
void main(void)
{
uart_init(); // 初始化 UART
/*
在 Linux 系统中,\n 表示回车和换行
在 Windows 系统中,\n 表示换行,\r 表示回到行首
*/
while(1) {
puts("hello world\r\n"); // 通过串口发送 "hello world" 字符串并换行
mysleep(200); // 延时 200 毫秒
}
return;
}
- 编写start.S
- 目的是为嵌入式系统中的主程序(如串口代码)提供一个基本的运行环境。
- 启动代码的作用是对处理器和堆栈等进行初始化,并最终跳转到主程序的入口点。
.global delay1s @ 全局定义 delay1s 延时函数
.text @ 定义文本段(代码段)
.global _start @ 全局定义 _start 入口函数
_start: @ 程序入口点,程序从此开始执行
b reset @ 跳转到 reset 处(复位处理)
ldr pc, _undefined_instruction @ 加载 undefined_instruction 异常处理器的地址
ldr pc, _software_interrupt @ 加载 software_interrupt 异常处理器的地址
ldr pc, _prefetch_abort @ 加载 prefetch_abort 异常处理器的地址
ldr pc, _data_abort @ 加载 data_abort 异常处理器的地址
ldr pc, _not_used @ 加载未使用异常的处理器地址
ldr pc, _irq @ 加载中断请求 (IRQ) 异常处理器的地址
ldr pc, _fiq @ 加载快速中断 (FIQ) 异常处理器的地址
_undefined_instruction: .word _undefined_instruction @ 定义未定义指令异常处理器的地址
_software_interrupt: .word _software_interrupt @ 定义软件中断异常处理器的地址
_prefetch_abort: .word _prefetch_abort @ 定义指令预取异常处理器的地址
_data_abort: .word _data_abort @ 定义数据访问异常处理器的地址
_not_used: .word _not_used @ 定义未使用异常处理器的地址
_irq: .word _irq @ 定义 IRQ 中断异常处理器的地址
_fiq: .word _fiq @ 定义 FIQ 快速中断异常处理器的地址
reset:
ldr r0,=0x40008000 @ 设置异常向量表基地址为 0x40008000
mcr p15, 0, r0, c12, c0, 0 @ 写入 Vector Base Address Register (VBAR),重新定位异常向量表
init_stack:
ldr r0,=stacktop @ 将栈顶地址加载到 r0
/******** svc 模式栈 ********/
mov sp,r0 @ 将栈顶指针加载到 sp,svc 模式的栈
sub r0,#128*4 @ 减少 512 字节空间用于 IRQ 模式栈
/******** irq 模式栈 ********/
msr cpsr,#0xd2 @ 切换到 IRQ 模式(CPSR 位设置)
mov sp,r0 @ 设置 IRQ 模式的栈指针
sub r0,#128*4 @ 减少 512 字节空间用于 FIQ 模式栈
/******** fiq 模式栈 ********/
msr cpsr,#0xd1 @ 切换到 FIQ 模式
mov sp,r0 @ 设置 FIQ 模式的栈指针
sub r0,#0 @ FIQ 模式使用 0 字节的栈空间
/******** abort 模式栈 ********/
msr cpsr,#0xd7 @ 切换到 Abort 模式
mov sp,r0 @ 设置 Abort 模式的栈指针
sub r0,#0 @ Abort 模式使用 0 字节的栈空间
/******** undefined 模式栈 ********/
msr cpsr,#0xdb @ 切换到 Undefined 模式
mov sp,r0 @ 设置 Undefined 模式的栈指针
sub r0,#0 @ Undefined 模式使用 0 字节的栈空间
/******** sys 和 usr 模式栈 ********/
msr cpsr,#0x10 @ 切换到用户模式 (SYS/USR)
mov sp,r0 @ 设置用户模式的栈指针
@ 为用户模式分配 1024 字节的栈空间
b main @ 跳转到 main 函数,进入主程序
delay1s: @ 定义 1 秒延时函数
ldr r4,=0x1ffffff @ 将一个大数加载到 r4,用作计数器
delay1s_loop:
sub r4,r4,#1 @ 每次循环 r4 减 1
cmp r4,#0 @ 检查 r4 是否为 0
bne delay1s_loop @ 如果 r4 不为 0,继续循环
mov pc,lr @ 返回调用函数
.align 4 @ 地址对齐
/**** swi_interrupt handler ****/
@ 异常处理中断的处理器可以在此处实现
.data @ 定义数据段
stack:
.space 4*512 @ 为栈分配 512 字节的空间
stacktop:
.word stack+4*512 @ 定义栈顶指针的初始位置
.end @ 程序结束标志
- 编写 Makefile
定义编译链接规则和清除不必要的东西
用于编译 ARM 裸机程序。它使用了 arm-none-linux-gnueabi 工具链来编译汇编和 C 代码,链接生成 ELF 可执行文件,并生成二进制文件。
# 目标名称 all 是默认目标,当执行 'make' 时会首先执行该目标中的指令
all:
# 使用 arm-none-linux-gnueabi-gcc 编译 start.S 文件为目标文件 start.o
# -fno-builtin: 禁用 GCC 的内置函数,避免使用标准库函数
# -nostdinc: 不使用标准头文件,适合裸机编程
# -c: 仅编译为目标文件,不进行链接
arm-none-linux-gnueabi-gcc -fno-builtin -nostdinc -c -o start.o start.S
# 使用 arm-none-linux-gnueabi-gcc 编译 main.c 文件为目标文件 main.o
# 这里同样不使用内置函数和标准头文件,适合裸机编程
arm-none-linux-gnueabi-gcc -fno-builtin -nostdinc -c -o main.o main.c
# 链接 start.o 和 main.o 目标文件,生成 main.elf 可执行文件
# -Tmap.lds: 使用自定义的链接脚本 map.lds 定义内存布局
# main.elf: 最终生成的可执行文件
arm-none-linux-gnueabi-ld start.o main.o -Tmap.lds -o main.elf
# 使用 arm-none-linux-gnueabi-objcopy 将 ELF 文件转换为纯二进制文件 main.bin
# -O binary: 指定输出格式为二进制文件
arm-none-linux-gnueabi-objcopy -O binary main.elf main.bin
# 使用 arm-none-linux-gnueabi-objdump 反汇编 ELF 文件,生成汇编代码文件 main.dis
# -D: 反汇编整个 ELF 文件
# 反汇编的输出被保存到 main.dis 文件中,便于阅读
arm-none-linux-gnueabi-objdump -D main.elf > main.dis
# clean 目标用于清理生成的中间文件和最终文件
clean:
# 删除所有备份文件、目标文件、ELF 文件、二进制文件和反汇编文件
# -f: 忽略文件不存在的错误
# *.bak: 删除所有扩展名为 .bak 的备份文件
# *.o: 删除所有目标文件
# main.elf: 删除生成的 ELF 可执行文件
# main.bin: 删除生成的二进制文件
# main.dis: 删除生成的反汇编文件
rm -rf *.bak start.o main.o main.elf main.bin main.dis
- 加入map.lds
- map.lds 链接脚本 定义了程序在内存中的布局,指定了代码段、数据段和未初始化数据段的起始地址和内容来源。
- 这份脚本非常适用于嵌入式系统开发,特别是当你需要精确控制程序的各个部分在内存中的位置时。
- 代码如下
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
# 指定生成文件的输出格式为 "elf32-littlearm",适用于 32 位 ARM 的小端格式
# "elf32-littlearm" 出现在三个位置,分别对应目标文件、二进制文件和目标机器的 ELF 格式
OUTPUT_ARCH(arm)
# 指定目标架构为 ARM,确保链接器知道处理的是 ARM 处理器的指令集
ENTRY(_start)
# 指定程序的入口地址为 _start,这是程序执行的起始点,通常定义在汇编文件 start.S 中
SECTIONS
{
. = 0x40008000;
# 设置程序在内存中的起始地址为 0x40008000,通常是裸机编程中设置的 RAM 基地址
# 所有代码段、数据段都从这个地址开始布局
. = ALIGN(4);
# 将当前地址对齐到 4 字节边界,以确保对齐要求满足 ARM 处理器的要求
# 对齐有助于提高内存访问效率,避免非对齐访问引发错误
.text :
{
start.o(.text)
# 将 start.o 文件中的 .text 段(代码段)加载到当前地址处
*(.text)
# 加载所有目标文件中的 .text 段(代码段),确保代码在合适的内存位置
}
. = ALIGN(4);
# 再次对齐到 4 字节边界,以确保段之间的正确对齐
.data :
{ *(.data) }
# 将所有目标文件中的 .data 段(数据段)加载到当前地址
# .data 段通常包含已初始化的全局变量和静态变量
. = ALIGN(4);
# 再次对齐到 4 字节边界,确保 bss 段的正确对齐
.bss :
{ *(.bss) }
# 将所有目标文件中的 .bss 段(未初始化的全局变量和静态变量)加载到当前地址
# .bss 段通常不占用文件大小,它只分配未初始化数据所需的空间
}
- make生成bin(二进制文件)烧录进入开发板即可,成功时会发送hello world 给PC