第四天的内容围绕裸机开发的编译流程、可执行文件结构和外设驱动展开,把核心点整理如下。
一、交叉编译工具链作用
裸机开发用的是arm-linux-gnueabihf工具链,四个核心工具各司其职:
gcc:编译器,把.c和.S源文件编译成.o目标文件,只做编译和汇编,不做链接。
ld:链接器,把多个.o目标文件拼接起来,指定程序运行的起始地址,生成elf可执行文件。
objcopy:格式转换工具,把elf文件转换成纯二进制的bin文件,去掉调试信息和段头,直接可以烧写到开发板运行。
objdump:反汇编工具,把elf文件反编译成汇编代码,用来调试和查看程序结构。
完整的编译链接流程:
bash
编译汇编启动文件
arm-linux-gnueabihf-gcc -g -c start.S -o start.o
编译C文件
arm-linux-gnueabihf-gcc -g -c main.c -o main.o
链接,指定代码段起始地址
arm-linux-gnueabihf-ld -Ttext 0x87800000 start.o main.o -o led.elf
转成二进制烧录文件
arm-linux-gnueabihf-objcopy -O binary -S -g led.elf led.bin
反汇编查看
arm-linux-gnueabihf-objdump -D led.elf > led.dis
二、可执行文件的段区别
elf文件按内容分成不同的段,各有分工:
.text段:存放代码和指令,是只读可执行的,程序运行的入口就在这里。
.data段:存放已经初始化的全局变量和静态变量,可读可写。
.rodata段:存放只读常量,比如字符串常量、const修饰的变量。
.bss段:存放初始化为0的全局变量和静态变量,不占用bin文件空间,程序运行时自动清0。
.common段:存放未初始化的全局变量,链接时会合并到bss段。
三、Makefile与链接脚本
Makefile三要素
目标:最终要生成的文件
依赖:生成目标需要用到的源文件
规则:生成目标的具体命令
工程大了之后可以用Makefile统一管理编译,避免每次手动敲多条命令。编译时还可以用 -I 指定头文件路径, -L 指定库文件路径。
链接脚本
比 -Ttext 更灵活的地址指定方式,用特殊语法的lds文件规划内存布局。IMX6ULL的DDR3地址范围是0x80000000~0xA0000000,共512MB,我们一般把程序放到0x87800000起始的位置。
四、LED和蜂鸣器实现流程
裸机操作外设本质就是操作寄存器,步骤都是统一的:
-
开时钟:外设默认时钟关闭,先配置CCM寄存器打开对应时钟。
-
引脚复用:配置IOMUXC寄存器,把引脚设置为GPIO功能。
-
引脚电气属性:配置上下拉、摆率等电气参数。
-
GPIO配置:设置GPIO方向为输出,然后写DR寄存器控制高低电平。
C语言操作寄存器的方式,直接通过地址指针访问:
// 定义寄存器地址
#define CCM_CCGR0 (*((volatile unsigned int *)0x020C4068))
// 打开所有时钟
CCM_CCGR0 = 0xFFFFFFFF;
这里必须加 volatile 关键字,作用是告诉编译器,对这个变量的访问不要做优化,每次都直接从内存读写。因为寄存器的值可能会被硬件改变,编译器优化掉的话会出问题。不仅变量前可以加,指针前加volatile表示指向的内存空间不被优化。
工程上一般会把外设驱动分层放到bsp目录,比如led.c、led.h,beep.c、beep.h,主函数只调用接口,结构更清晰。
补充
不同芯片的存储结构不一样,比如S3C2440用nor flash,地址从0x0开始,代码可以直接寻址;IMX6ULL用nand flash或者EMMC,不能直接寻址,程序要先搬到DDR里运行。