大部分情况下都是使用 C 语言去编写的。只是在开始部分用汇编来初始化一下 C 语言环境,比如初始化 DDR、设置堆栈指针 SP 等等,当这些工作都做完以后就可以进入 C 语言环境,也就是运行 C 语言代
码,一般都是进入 main 函数。所以我们有两部分文件要做:
①、汇编文件
汇编文件只是用来完成 C 语言环境搭建。
②、C 语言文件
C 语言文件就是完成我们的业务层代码的,其实就是我们实际例程要完成的功能。
配置vscode语言环境
这节课我们可以下载vscode去编写代码,用windows远程虚拟机中.具体配置过程如下:
首先下载版本低于等于1.85的。并把自动更新给关闭掉。下载链接为:1.85版本
然后安装插件:
然后这里要配置好你虚拟机端口的环境,确认22端口不在防火墙内。
在Ubuntu中打开终端。运行以下命令1下载安装SSH。
sudo apt update
sudo apt install openssh-server
等待安装完成后,运行以下命令查看SSH服务的状态。
sudo systemctl status ssh
接下来还要来配置防火墙,看看openssh是否在防火墙内。
查看防火墙中SSH服务的名字。
接着要知道虚拟机linux的ip地址是啥,并记录下来。
输入命令remote ssh并按下回车,
然后输入用户名和ip地址,
然后登陆时候会要求输入你linux的密码。输入后就可以打开你linux的文件夹了。
这里我选择我们这次编写程序的文件夹。
编写程序
首先创建一个文件夹,这里我创建的文件夹名字是:
接下来依次创建start.S,main.c,main.h。
在 STM32 中,启动文件 startup_stm32f10x_hd.s 就是完成 C 语言环境搭建的,这里我们也要使用start.S创建c语言环境,这样我们编写的c语言代码才能实现。
这里汇编程序为:
.global _start
_start:
mrs r0,cpsr
bic r0,r0,#0x1f
orr r0,r0,#0x13
msr cpsr,r0/* 将r0的数据写入到cpsr_c */
ldr sp,=0x80200000
b main
汇编程序这里文档给提供的很简单,表面看起来只操作了cpsr寄存器。以及给sp指针赋值,以及跳转到main函数。
- mrs r0, cpsr
mrs(Move to Register from Special register)指令用于将特殊寄存器的值移动到通用寄存器中。r0是目标寄存器,用于存储读取的值。cpsr是当前程序状态寄存器(Current Program Status Register),它包含了处理器的当前状态信息,如条件标志和当前处理器模式。这条指令的作用是将CPSR的当前值读取到r0寄存器中。
具体来说为啥要去操作这个寄存器,我们需要回顾Cortex-A7 MPCore 架构这部分知识点。
以前的 ARM 处理器有 7 中运行模型:User、FIQ、IRQ、Supervisor(SVC)、Abort、Undef和 System,其中 User 是非特权模式,其余 6 中都是特权模式。但新的 Cortex-A 架构加入了TrustZone 安全扩展,所以就新加了一种运行模式:Monitor,新的处理器架构还支持虚拟化扩展,因此又加入了另一个运行模式:Hyp,所以 Cortex-A7 处理器有 9 种处理模式,如表 所示模式:
所有的处理器模式都共用一个 CPSR 物理寄存器,因此 CPSR 可以在任何模式下被访问。CPSR 是当前程序状态寄存器,该寄存器包含了条件标志位、中断禁止位、当前处理器模式标志等一些状态位以及一些控制位。所有的处理器模式都共用一个 CPSR 必然会导致冲突,为此,除了 User 和 Sys 这两个模式以外,其他 7 个模式每个都配备了一个专用的物理状态寄存器,叫做 SPSR(备份程序状态寄存器),当特定的异常中断发生时,SPSR 寄存器用来保存当前程序状态寄存器(CPSR)的值,当异常退出以后可以用 SPSR 中保存的值来恢复 CPSR。
因为 User 和 Sys 这两个模式不是异常模式,所以并没有配备 SPSR,因此不能在 User 和Sys 模式下访问 SPSR,会导致不可预知的结果。由于 SPSR 是 CPSR 的备份,因此 SPSR 和CPSR 的寄存器结构相同,如图 6.3.2.1 所示:
这里我们用到的为:M[4:0]
M[4:0]:处理器模式控制位,含义如表 6.3.2.2 所示:
- bic r0, r0, #0x1f
bic(Bit Clear)指令用于对寄存器中的位进行清零操作。
第一个r0是目标寄存器,存储结果。
第二个r0是操作数,其内容会被修改。
0x1f是一个立即数,二进制表示为0001 1111。
这条指令的作用是将r0寄存器中对应于0x1f(即最低的5位)的位清零。这实际上是在清除CPSR中的模式位,因为CPSR的最低5位定义了处理器的模式。
3. orr r0, r0, #0x13
orr(Logical OR)指令用于执行逻辑或操作。
第一个r0是目标寄存器。
第二个r0是第一个操作数。
0x13是第二个操作数,二进制表示为0001 0011。
这条指令的作用是将r0寄存器的当前值与0x13进行逻辑或操作。0x13(十六进制)对应于SVC(Supervisor)模式的模式位。这条指令将处理器切换到SVC模式。
4. msr cpsr, r0
msr(Move to Special register from Register)指令用于将通用寄存器的值移动到特殊寄存器中。
cpsr是目标特殊寄存器。
r0包含了要写入的值。
这条指令的作用是将r0寄存器中的值(已经被修改为指示SVC模式的值)写回到CPSR中,从而改变处理器的模式。
ldr 指令设置 SVC 模式下的 SP 指针=0X80200000,因为 I.MX6U-ALPHA 开发板上的 DDR3 地址范围是 0X80000000~0XA0000000(512MB) 或 者0X80000000~0X90000000(256MB),不管是 512MB 版本还是 256MB 版本的,其 DDR3 起始地址都是 0X80000000。由于 Cortex-A7 的堆栈是向下增长的,所以将 SP 指针设置为 0X80200000,因此 SVC 模式的栈大小 0X80200000-0X80000000=0X200000=2MB,2MB 的栈空间已经很大了,如果做裸机开发的话绰绰有余。
我们上面编写的start.s 文件中却没有初始化 DDR3 的代码,但是却将 SVC 模式下的 SP 指针设置到了 DDR3 的地址范围中,这不会出问题吗?肯定不会的,DDR3 肯定是要初始化的,但是不需要在 start.s 文件中完成。在 9.4.2 小节里面分析 DCD 数据的时候就已经讲过了,DCD 数据包含了 DDR 配置参数,I.MX6U 内部的 Boot ROM 会读取 DCD 数据中的 DDR 配置参数然后完成 DDR 初始化的。
接下来就是编写C语言程序。
首先main.c
#include "main.h"
void clock_enable(void)
{
CCM_CCGR0 = 0xFFFFFFFF;
CCM_CCGR1 = 0xFFFFFFFF;
CCM_CCGR2 = 0xFFFFFFFF;
CCM_CCGR3 = 0xFFFFFFFF;
CCM_CCGR4 = 0xFFFFFFFF;
CCM_CCGR5 = 0xFFFFFFFF;
CCM_CCGR6 = 0xFFFFFFFF;
}
void gpio_enable(void)
{
SW_MUX_GPIO1_IO03 = 0x5;
/* 2、配置 GPIO1_IO03 的 IO 属性
*bit 16:0 HYS 关闭
*bit [15:14]: 00 默认下拉
*bit [13]: 0 kepper 功能
*bit [12]: 1 pull/keeper 使能
*bit [11]: 0 关闭开路输出
*bit [7:6]: 10 速度 100Mhz
*bit [5:3]: 110 R0/6 驱动能力
*bit [0]: 0 低转换率
*/
SW_PAD_GPIO1_IO03 = 0X10B0;
GPIO1_GDIR = 0X0000008;
GPIO1_DR = 0X0;
}
void led_state_init(int state)
{
if(state == 1)
{
GPIO1_DR = 0;
}
else if(state == 0)
{
GPIO1_DR = 1<<3;
}
}
void delay_ms(int ms)
{
int i,j;
for(i=0;i<ms;i++)
{
for(j=0;j<1000;j++);
}
}
int main()
{
clock_enable();
gpio_enable();
while(1)
{
led_state_init(1);
delay_ms(500);
led_state_init(0);
delay_ms(500);
}
return 0;
}
接下来是main.h编写:
#ifndef __Main_H
#define __Main_H
/* CCM 相关寄存器地址*/
#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)
/* IOMUX 相关寄存器地址 */
#define SW_MUX_GPIO1_IO03 *((volatile unsigned int *)0X020E0068)
#define SW_PAD_GPIO1_IO03 *((volatile unsigned int *)0X020E02F4)
/* * GPIO1 相关寄存器地址*/
#define GPIO1_DR *((volatile unsigned int *)0X0209C000)
#define GPIO1_GDIR *((volatile unsigned int *)0X0209C004)
#define GPIO1_PSR *((volatile unsigned int *)0X0209C008)
#define GPIO1_ICR1 *((volatile unsigned int *)0X0209C00C)
#define GPIO1_ICR2 *((volatile unsigned int *)0X0209C010)
#define GPIO1_IMR *((volatile unsigned int *)0X0209C014)
#define GPIO1_ISR *((volatile unsigned int *)0X0209C018)
#define GPIO1_EDGE_SEL *((volatile unsigned int *)0X0209C01C)
#endif
编写完所有程序后,我们要进行Makefile文件的编写,在该工程文件夹建立Makefile文件。
objs := start.o main.o
ledc.bin:$(objs)
arm-linux-gnueabihf-ld -Ttext 0X87800000 -o ledc.elf $^
arm-linux-gnueabihf-objcopy -O binary -S ledc.elf $@
arm-linux-gnueabihf-objdump -D -m arm ledc.elf > ledc.dis
%.o:%.s
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<
%.o:%.S
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<
%.o:%.c
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<
clean:
rm -rf *.o ledc.bin ledc.elf ledc.dis
我们可以看到要烧录的文件就是ledc.bin。它的依赖文件包括start.o和main.o。位于这个规则下面的有三条语句:
1:arm-linux-gnueabihf-ld -Ttext 0X87800000 -o ledc.elf $^:
这条命令使用ARM的链接器ld将所有依赖的目标文件($^,即start.o和main.o)链接成一个ELF格式的可执行文件ledc.elf。-Ttext 0X87800000指定程序的加载地址,-o选项后跟输出文件名ledc.elf。
2:arm-linux-gnueabihf-objcopy -O binary -S ledc.elf $@:
这条命令使用objcopy工具将ledc.elf转换成一个纯二进制格式的文件ledc.bin($@代表规则的目标,即ledc.bin)。-O binary指定输出格式为二进制,-S选项用于去除符号表和重定位信息,以减小最终二进制文件的大小。
3:arm-linux-gnueabihf-objdump -D -m arm ledc.elf > ledc.dis:
使用objdump工具以反汇编的形式打印ledc.elf的内容,并将输出重定向到ledc.dis文件中。这对于调试和理解程序的执行流程非常有用。-D表示反汇编所有内容,-m arm指定目标架构为ARM。
上面这三条规则需要start.o和main.o。所以会自动向下寻找规则并执行。
4:
%.o:%.s
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<
%.o:%.S
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<
%.o:%.c
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<
这三条规则说明了如何将汇编文件(.s或.S)和C源文件(.c)编译成目标文件(.o)。对于每种文件类型,使用arm-linux-gnueabihf-gcc编译器,带有-Wall(开启所有警告),-nostdlib(不链接标准库,因为这是裸机程序),-c(只编译不链接)。\(@代表规则的目标文件,\)<代表规则的第一个依赖,即输入文件。
接下来我们引入一个知识点,也就是链接脚本的概念来改写这句代码:arm-linux-gnueabihf-ld -Ttext 0X87800000 -o ledc.elf $^
是通过“-Ttext”来指定链接地址是 0X87800000 的,这样的话所有的文件都会链接到以 0X87800000 为起始地址的区域。但是有时候我们很多文件需要链接到指定的区域,或者叫做段里面。
链接脚本(Linker Script)是用于控制链接过程的一个重要工具,尤其在嵌入式系统和操作系统开发中非常重要。链接脚本为链接器(Linker)提供了一种方式来控制程序的内存布局,即决定程序的各个部分(或称为“段”)在内存中的位置。通过链接脚本,开发者可以细致地指定哪些代码和数据应该放在内存的哪个位置,这对于需要精确控制内存布局的场景(比如嵌入式设备或操作系统开发)来说非常重要。
接下来来了解下段的概念:
在编译和链接过程中,程序被分为多个段(Section),常见的段包括:
.text段:存放程序的执行代码。
.data段:存放初始化过的全局变量和静态变量。
.bss段:存放未初始化的全局变量和静态变量。
.rodata段:存放只读数据,比如字符串常量。
特殊的段,如.init和.fini段,分别用于存放程序初始化前和终止前需要执行的代码。
接下来创建文件,具体内容为:
SECTIONS{
. = 0X87800000;
.text :
{
start.o
main.o
*(.text)
}
.rodata ALIGN(4) : {*(.rodata*)}
.data ALIGN(4) : { *(.data) }
__bss_start = .;
.bss ALIGN(4) : { *(.bss) *(COMMON) }
__bss_end = .;
}
这段代码是一个链接脚本的示例,用于控制程序在内存中的布局。链接脚本通过指定不同的段(如.text
、.rodata
、.data
、.bss
等)在内存中的位置和顺序,来控制最终可执行文件的内存布局。下面是对这段链接脚本的详细分析:
段地址和顺序
SECTIONS{
. = 0X87800000;
SECTIONS
关键字开始定义段的映射和布局。这是链接脚本中用于定义输出段如何映射到内存中的部分。. = 0X87800000;
设置当前位置计数器.
的值为0X87800000
,意味着接下来定义的段将从这个地址开始放置。.
代表当前的地址指针,可以被视为“到目前为止已使用的内存大小”的累加器。
.text段
.text :
{
start.o
main.o
*(.text)
}
.text
定义了一个名为.text
的段,这个段通常包含程序的执行代码。start.o main.o
指定了start.o
和main.o
这两个目标文件中的.text
段要首先被放置在这个段中。这通常用于确保程序的入口点或初始化代码位于特定的位置。*(.text)
表示将所有输入文件中的.text
段都收集到这里。*
代表所有输入文件,.text
指的是这些文件中的.text
段。
.rodata段
.rodata ALIGN(4) : {*(.rodata*)}
.rodata
定义了一个名为.rodata
的段,用于存放只读数据,比如字符串常量等。ALIGN(4)
确保.rodata
段在内存中的地址是4的倍数,这有助于提高访问效率。{*(.rodata*)}
表示将所有输入文件中的.rodata
段和以.rodata
开头的任何其他段(比如.rodata.str1.1
等)都收集到这里。- 通配符的作用: * 在链接脚本中作为通配符使用,表示匹配所有的可能性。因此,.rodata* 匹配 .rodata 以及所有以 .rodata 开头的段名。这样做的好处是,即使在项目的后续开发中引入了新的以 .rodata 开头的段,链接脚本也无需修改,可以自动包含这些新段。
- 合并段:外层的星号表示对所有选中的输入文件或库中的对象文件进行操作。在这个上下文中,它告诉链接器将所有输入文件中名字匹配 .rodata* 的段合并到一起。这意味着,如果你有多个源文件,每个文件都可能产生一个或多个 .rodata 开头的段,链接器会将这些段全部合并,按照链接脚本指定的规则放置。
.data段
.data ALIGN(4) : { *(.data) }
.data
定义了一个名为.data
的段,用于存放已初始化的全局变量和静态变量。ALIGN(4)
确保.data
段在内存中的地址是4的倍数。{ *(.data) }
表示将所有输入文件中的.data
段都收集到这里。
.bss段
__bss_start = .;
.bss ALIGN(4) : { *(.bss) *(COMMON) }
__bss_end = .;
__bss_start = .;
在.bss
段开始前定义了一个符号__bss_start
,其值为当前的位置计数器.
的值,即.bss
段的起始地址。.bss
定义了一个名为.bss
的段,用于存放未初始化的全局变量和静态变量。ALIGN(4)
确保.bss
段在内存中的地址是4的倍数。{ *(.bss) *(COMMON) }
表示将所有输入文件中的.bss
段和COMMON
块都收集到这里。COMMON
块用于存放未明确放在其他段中的未初始化全局变量。__bss_end = .;
在.bss
段结束后定义了一个符号__bss_end
,其值为当前的位置计数器.
的值,即.bss
段的结束地址。
在上一小节中我们已经编写好了链接脚本文件:imx6ul.lds,我们肯定是要使用这个链接脚本文件的,将 Makefile 中的如下一行代码:
arm-linux-gnueabihf-ld -Ttext 0X87800000 -o ledc.elf $^改为:
arm-linux-gnueabihf-ld -Timx6ul.lds -o ledc.elf $^
其实就是将-T 后面的 0X87800000 改为 imx6ul.lds,表示使用 imx6ul.lds 这个链接脚本文件。修改完成以后使用新的 Makefile 和链接脚本文件重新编译工程,编译成功以后就可以烧写到 SD 卡中验证了