赞
踩
大部分情况下都是使用 C 语言去编写的。只是在开始部分用汇编来初始化一下 C 语言环境,比如初始化 DDR、设置堆栈指针 SP 等等,当这些工作都做完以后就可以进入 C 语言环境,也就是运行 C 语言代
码,一般都是进入 main 函数。所以我们有两部分文件要做:
①、汇编文件
汇编文件只是用来完成 C 语言环境搭建。
②、C 语言文件
C 语言文件就是完成我们的业务层代码的,其实就是我们实际例程要完成的功能。
这节课我们可以下载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函数。
#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
等)都收集到这里。.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
段的结束地址。Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。