一、深入理解ARMCPU架构及其指令格式、ARM汇编语言编程方法
1.汇编语言编程,实现LED灯
新建keil项目,选择芯片
选择运行环境以及配置
添加.s文件
汇编程序:
AREAMYDATA,DATA
AREAMYCODE,CODE
ENTRY
EXPORT__main
__main
MOVR0,#10
MOVR1,#11
MOVR2,#12
MOVR3,#13
;LDRR0,=func01
BL func01
;LDRR1,=func02
BL func02
BL func03
LDRLR,=func01
LDRPC,=func03
B.
func01
MOVR5,#05
BXLR
func02
MOVR6,#06
BXLR
func03
MOVR7,#07
MOVR8,#08
BXLR
点亮PC13程序:
LED0
EQU
0x4001100C
RCC_APB2ENR
EQU
0x40021018
GPIOC_CRH
EQU
0x40011004
Stack_Size
EQU
0x00000400
AREA STACK,NOINIT,READWRITE,ALIGN=3
Stack_Mem
SPACE Stack_Size
__initial_sp
AREA RESET,DATA,READONLY
__Vectors
DCD
__initial_sp
; Top of Stack
DCD
Reset_Handler
; Reset Handler
AREA |.text|, CODE, READONLY
THUMB
REQUIRE8
PRESERVE8
ENTRY
Reset_Handler
BL
LED_Init
MainLoop
BL LED_ON
BL Delay
BL LED_OFF
BL Delay
B MainLoop
LED_Init
PUSH {R0,R1,LR}
LDR R0,=RCC_APB2ENR
LDR R0,[R0]
ORR R0,R0,#0x10
LDR R1,=RCC_APB2ENR
STR R0,[R1]
LDR R0,=GPIOC_CRH
LDR R0,[R0]
BIC R0,R0,#0xF0000
ORR R0,R0,#0x30000
LDR R1,=GPIOC_CRH
STR R0,[R1]
MOV R0,#1
(LED灭)
LSL R0,R0,#13
LDR R1,=LED0
STR R0,[R1]
POP {R0,R1,PC}
LED_ON
PUSH {R0,R1,LR}
MOV R0,#0
LDR R1,=LED0
STR R0,[R1]
POP {R0,R1,PC}
LED_OFF
PUSH {R0,R1,LR}
MOV R0,#1
LSL R0,R0,#13
LDR R1,=LED0
STR R0,[R1]
POP {R0,R1,PC}
Delay
PUSH {R0-R2,LR}
MOVS R0,#0
MOVS R1,#0
MOVS R2,#0
DelayLoop0
ADDS R0,R0,#1
CMP R0,#330
BCC DelayLoop0
MOVS R0,#0
ADDS R1,R1,#1
CMP R1,#330
BCC DelayLoop0
MOVS R0,#0
MOVS R1,#0
ADDS R2,R2,#1
CMP R2,#15
BCC DelayLoop0
POP {R0-R2,PC}
END
2.对比C语言寄存器
C语言寄存器模式代码:
#include “stm32f10x.h”
void delay(u16 num)
{
u16 i,j;
for(i=0;i<num;i++)
for(j=0;j<0x800;j++);
}
int main(void)
{
RCC->APB2ENR = 0x0000001C;
while(1)
{
GPIOB->ODR &= ~(1 << 5);
delay(100);
GPIOB->ODR |= (1 << 5);
delay(200);
}
}
分析:
汇编语言的hex比C语言寄存器代码小。
汇编语言直接使用CPU指令操作寄存器,而C语言寄存器经过编译后会产生额外的指令。C代码编译后会自动添加一些启动和初始化代码。纯汇编需要手动定义程序入口和段属性。
二、熟悉ARM 汇编语言与C语言混合编程方式,掌握反汇编工具
1.在Keil中修改C程序的 主函数为其他名称
C程序主函数名并非必须为main。在ARM开发中,入口函数的名称可以自定义。
在 startup 文件中修改入口点声明
2.IDA Pro工具
加载hex/bin文件
使用 IDA Pro 对给定的 hex(bin)文件进行反汇编分析,找出控制LED 灯闪烁的相关代码段。这可能涉及对GPIO端口配置、延时函数以及LED状态 切换等相关代码的识别。在反汇编代码中,查找与(PB0)端口操作相关的指令, 如GPIO初始化、输出电平设置以及可能的延时循环等部分。
在 IDA Pro 中对反汇编后的代码进行修改,将与 PC13 相关的操作改为针 对 PA1 的操作。修改完成后,使用 IDA Pro 的重新汇编功能或其他相关工具 将修改后的汇编代码转换为二进制文件。将生成的二进制文件下载到目标硬件 平台进行验证,观察 PA1 LED 灯是否按照预期进行闪烁。在验证过程中,可 使用示波器、逻辑分析仪等工具监测 PA1 端口的电平变化,确保程序功能修 改正确。
源代码:
IDA汇编:
三、掌握gcc编译工具集编译、链接源程序生成二进制应用程序
1.配置C/C++环境
安装gcc:sudo apt-get install gcc
安装g++:sudo apt-get install g++
安装gbd:sudo apt-get install gdb
2.安装、配置VScode
3.安装、配置arm-none-eabi-gcc 交叉编译工具链
下载解压完成后,进入 “arm-gnu-toolchain-13.2.Rel1-x86_64-arm-none-eabi”文件夹进入“bin”文件夹。
打开profile 文件,在最后输入:
export PATH=$PATH:/home/yml/mondrian/arm-gnu-toolchain-13.2.Rel1-x86_64-arm-none-eabi/bin
执行:source profile
4.安装CubeMX
测试程序
四、了解ARM 应用程序的上电复位初始化、startup启动到跳转到C语言main 函数入库的过程
1.STM32的启动过程
复位后启动模式的选择
复位方式有三种:上电复位,硬件复位和软件复位。当产生复位,并且离开
复位状态后,CM3 内核做的第一件事就是读取下列两个32 位整数的值:
(1)从地址 0x00000000 处取出堆栈指针 MSP 的初始值,该值就是栈顶
地址。
(2)从地址 0x00000004 处取出程序计数器指针 PC 的初始值,该值指向
复位后执行的第一条指令。
内核在离开复位状态后会从映射的地址中取值给栈指针MSP及程序指针 PC,然后执行指令,我们一般以存储器的类型来区分自举过程,例如内部FLASH 启动方式、内部SRAM启动方式以及系统存储器启动方式。
内部FLASH启动方式:
当芯片上电后采样到BOOT0引脚为低电平时,0x00000000和0x00000004 地址被映射到内部FLASH的首地址0x08000000和0x08000004。因此,内核 离开复位状态后,读取内部FLASH的0x08000000地址空间存储的内容,赋值 给栈指针MSP,作为栈顶地址,再读取内部FLASH的0x08000004地址空间存 储的内容,赋值给程序指针PC,作为将要执行的第一条指令所在的地址。具备 这两个条件后,内核就可以开始从PC指向的地址中读取指令执行了。
内部SRAM启动方式:
当芯片上电后采样到BOOT0和BOOT1引脚均为高电平时, 0x00000000和0x00000004地址被映射到内部SRAM的首地址0x20000000和 0x20000004,内核从SRAM空间获取内容进行自举。 在实际应用中,由启动文件starttup_stm32f10x.s决定了0x00000000和 0x00000004地址存储什么内容,链接时,由分散加载文件(sct)决定这些内容的 绝对地址,即分配到内部FLASH还是内部SRAM。 这里的启动文件和分散加载文件有点不好理解,当说“由启动文件决定了 0x00000000和0x00000004地址存储什么内容”时,指的是启动文件定义了这两 个关键地址处的内容:一个是堆栈指针的初始值,另一个是指向复位处理程序的 指针。而“由分散加载文件决定这些内容的绝对地址”则意味着,尽管启动文件设 定了逻辑上的地址,但是具体的物理地址(即这些内容实际上位于FLASH还是 SRAM)是由分散加载文件来决定的。这允许开发者根据需要调整最终的内存布 局,同时保持启动代码的逻辑不变。
系统存储器启动方式:
当芯片上电后采样到BOOT0引脚为高电平,BOOT1为低电平时,内核将 从系统存储器的0x1FFFF000及0x1FFFF004获取MSP及PC值进行自举。系 统存储器是一段特殊的空间,用户不能访问,ST公司在芯片出厂前就在系统存 储器中固化了一段代码。因而使用系统存储器启动方式时,内核会执行该代码, 该代码运行时,会为ISP提供支持(InSystemProgram),如检测USART1/2、CAN2 及USB通讯接口传输过来的信息, 并根据这些信息更新自己内部FLASH的内 容,达到升级产品应用程序的目的,因此这种启动方式也称为ISP启动方式。
2.STM32的启动方式实验
代码:
#include “led.h”
#include “delay.h”
//#include"key.h"
#include “sys.h”
#include “usart.h”
#include <stdio.h>
#include <stdlib.h>
int k1= 1; //已初始化全局int型变量k1
int k2; //未初始化全局int型变量k2
staticint k3 = 2;//已初始化静态全局int型变量k3
staticint k4; //未初始化静态全局int型变量k4
int main(void)
{
delay_init(); //延时函数初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优
先级,2位响应优先级
uart_init(115200); //串口初始化为115200
LED_Init(); //LED端口初始化
//KEY_Init(); //初始化与按键连接的硬件接口
while(1)
{
static int m1 = 2; //已初始化静态局部int型变量m1
static int m2; //未初始化静态局部int型变量m2
int i1; //未初始化局部int型变量i1
int i2; //未初始化局部int型变量i2
char *p; //未初始化局部char型指针变量p
char str[10] =“hello”;//已初始化局部char型数组str
char *var1 = “123456”; //已初始化局部char型指针变量var1
char *var2 = “abcdef”; //已初始化局部char型指针变量var2
int *p1 =malloc(4); //已初始化局部int型指针变量p1
int *p2 =malloc(4); //已初始化局部int型指针变量p2
printf(“栈区-变量地址\r\n”);
printf(“未初始化局部int型变量i1 :0x%p\r\n”,&i1);
printf(“未初始化局部int型变量i2 :0x%p\r\n”,&i2);
printf(“未初始化局部char型指针变量p :0x%p\r\n”,&p);
printf(“已初始化局部char型数组str :0x%p\r\n”,str);
//test();
printf(“\n堆区-动态申请地址\r\n”);
printf(“已初始化局部int型指针变量p1 :0x%p\r\n”,p1);
printf(“已初始化局部int型指针变量p2 :0x%p\r\n”,p2);
printf(“\n.bss段地址\r\n”);
printf(“未初始化全局int型变量k2 :0x%p\r\n”,&k2);
printf(“未初始化静态全局int型变量k4 :0x%p\r\n”,&k4);
printf(“未初始化静态局部int型变量m2 :0x%p\r\n”,&m2);
printf(“\n.data段地址\r\n”);
printf(“已初始化全局int型变量k1 :0x%p\r\n”,&k1);
printf(“已初始化静态全局int型变量k3 :0x%p\r\n”,&k3);
printf(“已初始化静态局部int型变量m1 :0x%p\r\n”,&m1);
printf(“\n常量区地址\r\n”);
printf(“已初始化局部char型指针变量var1:0x%p\r\n”,var1);
printf(“已初始化局部char型指针变量var2:0x%p\r\n”,var2);
printf(“\n代码区地址\r\n”);
printf(“程序代码区main函数入口地址:0x%p\r\n”, &main);
free(p1);
free(p2);
}
}
串口输出结果:
从中可以分析,main函数入口地址、常量区及代码区的存储地址都在 0x8000000开始的,说明程序成功从FALSH运行;全局和静态变量、栈区的地 址也都分配在0x20000000储存地址(高地址)处,堆区的地址分配在0x00000000 处。