2. 开发环境搭建:交叉编译工具链安装、Makefile基础、链接脚本(.ld)编写入门
好,咱们正式开始动手了。
上一章我们聊了嵌入式图像处理到底是个什么玩意儿。这一章,咱们得先把家伙事儿备齐。你想想看,没有工具,再牛的算法也跑不起来。
我个人习惯把环境搭建分成三块:交叉编译工具链、Makefile、链接脚本。这三样东西,说白了就是:
- 工具链:让你能在PC上编译出ARM芯片能跑的程序。
- Makefile:让你不用每次都敲一长串命令。
- 链接脚本:告诉编译器,你的代码该放哪儿,数据该放哪儿。
嗯,咱们一个一个来。
2.1 交叉编译工具链安装
什么是交叉编译?
你的电脑是x86架构,但你的目标板是ARM架构。你不能直接在PC上编译一个程序然后扔到ARM板上跑——指令集都不一样。所以我们需要一个交叉编译器,它在x86上运行,但生成ARM的机器码。
核心概念: 交叉编译 = 在A平台上编译出B平台能运行的程序。
我刚开始做嵌入式那会儿,就犯过傻。直接在Ubuntu上用gcc编译了一个程序,拷到ARM板上,执行报错:./hello: cannot execute binary file: Exec format error。嗯,当时我还纳闷了半天,后来才反应过来——架构不对。
2.1.1 安装ARM交叉编译器
以最常用的ARM Cortex-A系列为例,我们使用arm-linux-gnueabihf-这套工具链。
方法一:apt安装(推荐新手)
sudo apt-get update
sudo apt-get install gcc-arm-linux-gnueabihf
sudo apt-get install g++-arm-linux-gnueabihf
装完之后,验证一下:
arm-linux-gnueabihf-gcc --version
看到版本号,说明装好了。
方法二:从Linaro官网下载(推荐老手)
我个人习惯用Linaro的预编译工具链,版本更新快,优化也好。下载后解压,然后配置环境变量:
export PATH=$PATH:/path/to/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin
小技巧: 把上面这行加到 ~/.bashrc 里,这样每次打开终端就不用重新export了。
2.1.2 验证工具链
写一个最简单的C程序:
// hello.c
#include <stdio.h>
int main() {
printf("Hello, Embedded World!\n");
return 0;
}
用交叉编译器编译:
arm-linux-gnueabihf-gcc hello.c -o hello_arm
查看生成的文件信息:
file hello_arm
你会看到类似这样的输出:
hello_arm: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3
看到ARM字样,说明编译成功了。嗯,这一步走通了,后面就好办了。
注意: 如果你用的是64位ARM芯片(比如Cortex-A53),请使用 aarch64-linux-gnu- 前缀的工具链。别搞混了,我见过有人拿32位工具链去编译64位程序,结果链接阶段各种报错。
2.2 Makefile基础
为什么要学Makefile?
你想想看,一个图像处理项目,少说也有几十个源文件。每次改一个文件,你都要重新敲一遍编译命令?那不得累死。Makefile就是帮你自动化这件事的。
说白了,Makefile就是一套规则:告诉make工具,哪个文件依赖哪个文件,怎么编译,怎么链接。
2.2.1 最简单的Makefile
# Makefile
all: hello_arm
hello_arm: hello.c
arm-linux-gnueabihf-gcc hello.c -o hello_arm
clean:
rm -f hello_arm
使用方法:
make # 编译
make clean # 清理
这里要注意:gcc前面那个缩进必须是Tab键,不能是空格。我当年在这个坑上栽过,Makefile报错missing separator,查了半天才发现是空格的问题。
2.2.2 带变量的Makefile
实际项目中,我们不会把编译器名字写死。用变量管理更方便:
# Makefile
CROSS_COMPILE = arm-linux-gnueabihf-
CC = $(CROSS_COMPILE)gcc
CFLAGS = -Wall -O2 -march=armv7-a
TARGET = image_process
SRCS = main.c utils.c filter.c
OBJS = $(SRCS:.c=.o)
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
clean:
rm -f $(OBJS) $(TARGET)
解释几个关键点:
$@:目标文件名$^:所有依赖文件$<:第一个依赖文件%.o: %.c:模式规则,把所有.c文件编译成对应的.o文件
个人经验: 我习惯把CFLAGS里加上-march=armv7-a这样的架构优化参数。对于图像处理这种计算密集型任务,CPU指令集的优化能带来10%-20%的性能提升。别小看这点提升,在嵌入式设备上,每一分性能都很宝贵。
2.2.3 多目录管理
当项目变大,源文件会分散在不同目录。比如:
project/
├── src/
│ ├── main.c
│ ├── filter.c
│ └── utils.c
├── inc/
│ ├── filter.h
│ └── utils.h
└── Makefile
对应的Makefile:
CROSS_COMPILE = arm-linux-gnueabihf-
CC = $(CROSS_COMPILE)gcc
CFLAGS = -Wall -O2 -I./inc
SRC_DIR = ./src
OBJ_DIR = ./obj
TARGET = image_process
SRCS = $(wildcard $(SRC_DIR)/*.c)
OBJS = $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(SRCS))
all: $(TARGET)
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
$(OBJ_DIR):
mkdir -p $@
clean:
rm -rf $(OBJ_DIR) $(TARGET)
这里用了wildcard和patsubst两个函数,自动扫描源文件并生成目标文件路径。嗯,这样你新增一个.c文件,Makefile不用改,省心。
2.3 链接脚本(.ld)编写入门
链接脚本,很多人觉得难,其实没那么玄乎。
它的作用就是告诉链接器:你的代码段放哪个地址?数据段放哪个地址?堆栈设多大?
在无操作系统的裸机环境下,没有MMU帮你做地址映射,所以你必须自己定义好内存布局。
2.3.1 一个典型的链接脚本
/* link.ld */
OUTPUT_FORMAT("elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
/* 代码段从0x80000000开始(常见于SDRAM起始地址) */
. = 0x80000000;
.text : {
*(.text)
*(.rodata)
}
/* 数据段 */
. = ALIGN(4);
.data : {
*(.data)
}
/* BSS段(未初始化全局变量) */
. = ALIGN(4);
.bss : {
__bss_start = .;
*(.bss)
*(COMMON)
__bss_end = .;
}
/* 堆栈设置 */
. = ALIGN(8);
. = . + 0x10000; /* 64KB堆栈 */
__stack_top = .;
}
关键点解释:
OUTPUT_FORMAT:指定输出格式,ARM小端模式ENTRY(_start):程序入口,通常是汇编写的启动代码. = 0x80000000:当前地址设为0x80000000,后续段从这里开始放*(.text):把所有目标文件的.text段合并到这里__bss_start和__bss_end:BSS段的起止地址,启动代码里要用这两个符号来清零BSS段
我曾经踩过的坑: 有一次做摄像头图像采集项目,程序跑起来总是一卡一卡的。查了两天,最后发现是堆栈设得太小,才4KB。图像处理函数里有个局部数组,直接栈溢出了。从那以后,我习惯在链接脚本里至少留64KB的堆栈空间,如果用到递归或者大数组,直接给256KB。
2.3.2 带RAM和ROM分离的链接脚本
很多嵌入式芯片有片内Flash和片内RAM。代码放在Flash里,运行时数据放在RAM里。这时候链接脚本要复杂一些:
/* link.ld */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS
{
/* 代码段放在Flash */
.text : {
*(.text)
*(.rodata)
} > FLASH
/* 初始化数据段:运行时在RAM,但初始值保存在Flash */
.data : {
__data_start = .;
*(.data)
__data_end = .;
} > RAM AT > FLASH
/* BSS段 */
.bss : {
__bss_start = .;
*(.bss)
*(COMMON)
__bss_end = .;
} > RAM
/* 堆栈 */
. = ALIGN(8);
. = . + 0x10000;
__stack_top = .;
}
这里有个关键点:> RAM AT > FLASH。意思是:.data段运行时在RAM里,但它的初始值保存在Flash里。启动代码需要把这段数据从Flash拷贝到RAM。
为什么这么做? 因为RAM掉电丢失,Flash掉电不丢失。程序启动时,从Flash把初始化数据拷贝到RAM,然后才能正常使用全局变量。
2.3.3 在Makefile中使用链接脚本
LDFLAGS = -T link.ld -nostdlib
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ -lgcc
-T link.ld指定使用我们写的链接脚本。-nostdlib表示不使用标准C库的启动文件,因为裸机环境下没有操作系统帮你做这些初始化工作。
2.4 实战:编译第一个裸机程序
好,理论说完了,咱们动手试试。
步骤1:准备文件
创建三个文件:
start.S:汇编启动代码main.c:主程序link.ld:链接脚本Makefile:编译规则
start.S
.global _start
_start:
/* 设置栈指针 */
ldr sp, =__stack_top
/* 清零BSS段 */
ldr r0, =__bss_start
ldr r1, =__bss_end
mov r2, #0
loop:
cmp r0, r1
bge done
str r2, [r0], #4
b loop
done:
/* 跳转到main函数 */
bl main
/* 死循环 */
b .
main.c
void main(void) {
volatile unsigned int *led = (unsigned int *)0x40020C14;
*led = 0x01; // 点亮LED(假设地址)
while(1);
}
编译并查看结果
make
arm-linux-gnueabihf-objdump -d image_process > disasm.txt
打开disasm.txt,你会看到:
80000000 <_start>:
80000000: e59fd000 ldr sp, [pc, #0]
80000004: e59f0018 ldr r0, [pc, #24]
...
看到地址从0x80000000开始,说明链接脚本生效了。
我的建议: 第一次做裸机开发,别急着写复杂算法。先点亮一个LED,证明你的编译环境、链接脚本、启动代码都是对的。这一步走通了,后面图像处理的事情才有基础。
2.5 本章小结
这一章我们干了三件事:
- 装了交叉编译工具链——让你的PC能编译ARM程序
- 写了Makefile——让编译自动化,不用每次都敲长命令
- 写了链接脚本——告诉编译器内存怎么布局
这些东西看起来琐碎,但它们是嵌入式开发的基石。我见过太多人一上来就写算法,结果编译不过、链接报错、运行崩溃,最后发现是环境没搭好。
嗯,磨刀不误砍柴工。下一章,咱们开始真正接触图像处理——从BMP文件格式说起。