作者 | 将狼才鲸 |
---|---|
创建日期 | 2022-11-05 |
Gitee工程和源码地址:才鲸嵌入式 / ARM-Cortex-M3从汇编到C_从Boot到应用教程
CSDN文章阅读地址:ARM Cortex-M3从汇编到C,从Boot到应用的教程
Bilibili视频讲解地址(待完成):才鲸嵌入式
工程名称 | 作用 |
---|---|
01_Hello_world | 使用Keil的模拟器在虚拟终端输出Hello world |
02_Keil_boot_comments | Keil自带汇编boot的注释 |
03_Self_assembler_boot | 自行实现汇编boot |
04_Uart_loopback | 串口收发回环 |
… | … |
3)03_Self_assembler_boot
-
不使用官方自带的boot,自己用汇编写boot并引导C语言main(),成功打印输出,并编写注释,看到系统启动过程中的每一步。
-
工程和源码在本文档同级目录\src\03_Self_assembler_boot\下
- 创建工程时,只选择选择CMSIS中的Core,不选择Device中的Startup。
- ARM汇编的格式有两种:用Keil MDK ARM格式的汇编和GNU格式的汇编。
- ARM格式的汇编关键字多为大写,编译使用armasm编译器和armclang编译器都可以;GNU格式关键字多为小写,要使用armclang 集成编译器;ARM的编译工具套件当前版本为ARM Compiler Version 6。
- 我这里使用Keil MDK ARM格式的汇编,编译器让Keil自动识别文件自动选择,当前使用的是armclang。
- Keil MDK ARM格式的汇编伪指令详见子文档《02_Keil_ARM汇编伪指令.md》
-
参考网址:
-
实际工程中将汇编分成了好几个文件,下面列出的源码先将汇编部分都放在一起,便于观看,实际使用的文件在工程的源码中列出:
; 使用Keil自动生成时,也可用纯C写Boot相关的配置
INCLUDE YOUR_CONFIG.INC ; #include "YOUR_NAME.INC" 包含头文件
; 用户自定义宏定义
YOUR_CONFIG1 EQU 0x01 ; 类似于#define宏定义,用不同的配置选项配置程序
YOUR_CONFIG2 EQU 0x01
PRESERVE8 ; 指定当前文件要求堆栈八字节对齐
THUMB ; 使用THUMB指令集,不使用ARM指令集
; 定义堆,堆是malloc主动分配内存的位置
Heap_Size EQU 0 ; Heap_Size是MDK指定的堆空间长度名称;不用malloc分配的堆没什么用,所以长度设置为0
IF Heap_Size != 0 ; IF ELSE ENDIF和同名宏定义的含义类似
AREA HEAP, NOINIT, READWRITE, ALIGN=3 ; 申明开辟名为HEAP的内存,不写入值初始化,可读可写,2^3 8字节对齐
EXPORT __heap_base ; MDK指定的名称,堆起始地址位置
EXPORT __heap_limit ; MDK指定的名称,堆结束地址位置
__heap_base
Heap_Mem SPACE Heap_Size ; 开始分配指定长度的内存
__heap_limit
ENDIF
; 定义栈,栈是为全局变量自动分配空间的位置
Stack_Size EQU (4096) ; Stack_Size是MDK指定的栈空间长度名称
AREA STACK, NOINIT, READWRITE, ALIGN=3 ; 申明开辟名为STACK的内存,不写入值初始化,可读可写,2^3 8字节对齐
EXPORT __stack_limit ; MDK指定的名称,栈起始地址位置
EXPORT __initial_sp ; MDK指定的名称,栈结束地址位置
__stack_limit
Stack_Mem SPACE Stack_Size ; 开始分配指定长度的一片连续的内存
__initial_sp
AREA RESET, DATA, READONLY ; 定义数据段,名字为RESET;上电后首先运行的函数地址
EXPORT __Vectors ; 输出ARM CMSIS中需要用到的一些标号,__Vectors函数在后续定义
EXPORT __Vectors_End
EXPORT __Vectors_Size
EXPORT Default_Interrupt_Handler ; 中断入口
IMPORT __initial_sp
; 申明异常和中断入口
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; -14 NMI Handler
DCD HardFault_Handler ; -13 Hard Fault Handler
DCD MemManage_Handler ; -12 MPU Fault Handler
DCD BusFault_Handler ; -11 Bus Fault Handler
DCD UsageFault_Handler ; -10 Usage Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; -5 SVCall Handler
DCD DebugMon_Handler ; -4 Debug Monitor Handler
DCD 0 ; Reserved
DCD PendSV_Handler ; -2 PendSV Handler
DCD SysTick_Handler ; -1 SysTick Handler
; Interrupts
DCD Interrupt_Handler_0
; …… 省略其它中断 ……
DCD Interrupt_Handler_45 ; BCD:分配存储单元
__Vectors_End
__Vectors_Size EQU __Vectors_End - __Vectors
; 类似于宏定义函数
MACRO ; 宏定义函数的开始
Set_Default_Handler $Handler_Name ; 前一个时宏定义函数的名字,后面是要操作的对象
$Handler_Name PROC ; 定义子程序
EXPORT $Handler_Name [WEAK] ; 输出函数名;[WEAK]虚函数,可定义可不定义
B . ; 死循环
ENDP ; 子程序定义结束
MEND ; 宏定义函数结束
AREA |.text|, CODE, READONLY ; 定义.text代码段,只读
; 申明默认的异常和中断处理函数
Set_Default_Handler Reset_Handler
Set_Default_Handler NMI_Handler
Set_Default_Handler HardFault_Handler
Set_Default_Handler MemManage_Handler
Set_Default_Handler BusFault_Handler
Set_Default_Handler UsageFault_Handler
Set_Default_Handler SVC_Handler
Set_Default_Handler DebugMon_Handler
Set_Default_Handler PendSV_Handler
Set_Default_Handler SysTick_Handler
Set_Default_Handler Default_Interrupt_Handler
Set_Default_Handler Interrupt_Handler_0
; …… 省略其它中断 ……
Set_Default_Handler Interrupt_Handler_45
; 各种程序
; Reset_Handler是板子上电后首先执行的位置,它由异常中断的跳转来实现
Reset_Handler PROC ; 程序名 PROC 程序内容 ENDP 程序结束
EXPORT Reset_Handler
IMPORT __hardwareInit ; 自己编写的C程序,在里面初始化各种硬件配置
IMPORT __main ; main()函数入口
BL __hardwareInit ; 调用初始化硬件的汇编函数
BL __main ; 跳转到main()函数
BL cpuStall ; 程序退出后一直死循环
ALIGN
ENDP
cpuStall PROC
EXPORT cpuStall
B . ; 死循环
ENDP
END ; 通知编译器已经到了该源文件的结尾了
__hardwareInit PROC
EXPORT __hwInitialize
PUSH {R0,R1,R2,R3,LR} ; 压栈
; 配置GPIO输出
; 配置PLL系统主频,将主频从晶振原有的频率提高到实际的工作频率
; 初始化串口
; 初始化其它外设
POP {R0,R1,R2,R3,LR} ; 弹栈
BX LR ; 跳转到LR寄存器里的地址执行,也就是跳转回被调用的地方
ALIGN
ENDP
; 其它的.inc汇编头文件中要做的事
; 定义各个硬件模块的地址
; 定义所有中断号
-
此处列出工程中的各个文件:
-
startup.asm:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; @brief 程序执行的第一行代码所在处,中断向量表
; @note File format: UTF-8
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
INCLUDE config.inc
PRESERVE8 ; 8字节对齐,函数标号所在处一定要4字节对齐才能运行正常
THUMB ; 汇编使用thumb指令集
;;
; \brief 中断向量表
; \details 从地址0开始的数据段,0地址存放栈顶地址,4地址是复位中断,后面紧接着的是其它中断
;;
AREA RESET, DATA, READONLY ; 定义名为‘RESET’的数据段,只读
; 声明本文件中定义的函数
EXPORT __Vectors ; 声明定义了第一个中断的入口地址
EXPORT __Vectors_End ; 声明定义了最后一个中断的入口地址
EXPORT __Vectors_Size ; 声明中断的地址总量
EXPORT Default_Interrupt_Handler ; 声明没有指定中断函数时的默认中断处理函数
IMPORT __Init_Sp ; 声明其它文件中定义的栈顶地址
;;
; \brief 在各个中断入口调用中断处理函数
; \details __Vectors是整个中断入口的初始地址
;;
__Vectors DCD __Init_Sp ; 栈顶地址
DCD Reset_Handler ; 复位中断
;TODO: 添加其它的所有中断入口
__Vectors_End
__Vectors_Size EQU __Vectors_End - __Vectors
;;
; \brief 中断和异常处理的宏定义函数
; \details 这里定义的函数里面只有死循环,可以在别处重写这些中断处理函数
;;
MACRO ; 宏定义函数
Set_Default_Handler $Handler_Name
$Handler_Name PROC
EXPORT $Handler_Name [WEAK] ; 该函数可重定义,也可以不定义
B . ; 死循环
ENDP
MEND
;;
; \brief 声明默认的中断处理函数
; \details 该函数没有定义也可以
;;
AREA |.text|, CODE, READONLY ; 定义只读代码段
Set_Default_Handler Reset_Handler
Set_Default_Handler Default_Interrupt_Handler
ALIGN ; 取消8字节对齐
END ; 文件尾
- reset.asm:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; @brief 程序执行的第二行代码所在处
; @author 将狼才鲸
; @date 2022-11-06
; @note File format: UTF-8
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;/*
; * SPDX-License-Identifier: Apache-2.0
; *
; * Licensed under the Apache License, Version 2.0 (the License); you may
; * not use this file except in compliance with the License.
; * You may obtain a copy of the License at
; *
; * www.apache.org/licenses/LICENSE-2.0
; *
; * Unless required by applicable law or agreed to in writing, software
; * distributed under the License is distributed on an AS IS BASIS, WITHOUT
; * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; * See the License for the specific language governing permissions and
; * limitations under the License.
; */
PRESERVE8 ; 指定当前文件要求堆栈八字节对齐
THUMB ; 使用THUMB指令集,不使用ARM指令集 ; 头文件包含,里面只定义了一些宏定义
AREA |.text|, CODE, READONLY ; 定义.text代码段,只读
;;
; \brief 复位中断处理函数
; \details 程序执行的第一行代码是0地址的复位中断,然后才直接调用到这个函数,
; 中断向量表在startup.asm中定义
;;
Reset_Handler PROC
EXPORT Reset_Handler ; 声明函数,声明后可以在别处调用
;IMPORT __hw_init ; 声明硬件初始化函数
;IMPORT __main ; 声明c语言的main函数,ARM Compiler 5版本的编译器编译时,会在c语言函数前面加上两个下划线
IMPORT main ; 当前ARM Compiler 6汇编中直接调用C函数就可以了
;BL __hw_init ; 如果有必要,调用硬件初始化的函数,如配置PLL之类的
;BL __main ; 跳转到C语言main()函数
BL main
BL Cpu_Stall ; 如果main函数中不是死循环,函数退出了,则直接在这里死循环
ALIGN ; 取消8字节对齐,标号地址都是要4字节对齐的,否则程序会出错
ENDP ; 函数结束
;;
; \brief 死循环函数
;;
Cpu_Stall PROC
EXPORT Cpu_Stall
B . ; 跳转到本行地址,也就是死循环
ENDP
END ; 编译汇编时,文件尾要先写END,否则编译时从第一条语句就会报错,而且只会提示指令不支持
- stack.asm:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; @brief 定义栈地址,类似于宏定义,并不具体执行代码
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
INCLUDE config.inc
PRESERVE8
THUMB
;;
; \brief 定义栈尺寸,必须8字节对齐
;;
Stack_Size EQU (CONFIG_STACK_K_SIZE * 1024) ; 4K
; 申明开辟名为STACK的内存,不写入值初始化,可读可写,2^3 8字节对齐
AREA STACK, NOINIT, READWRITE, ALIGN=3
EXPORT __Stack_Limit ; 声明栈底地址变量
EXPORT __Init_Sp ; 声明栈顶地址变量
__Stack_Limit
Stack_Mem SPACE Stack_Size ; 开辟4K内存
__Init_Sp
ALIGN ; 取消8字节对齐
END ; 文件尾
- config.inc:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; @brief 程序的共用宏定义配置
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
CONFIG_STACK_K_SIZE EQU 0x04 ; 4K栈空间
CONFIG_HEAP_K_SIZE EQU 0x00 ; 堆空间无
END ; 文件尾
- main.c:
/******************************************************************************
* \brief 程序除boot外执行的第一个C语言函数
* \details main函数里什么都没有,这个工程主要是自己用汇编写了M3的boot代码,
* 并成功用汇编引导了C语言main函数
* \author 将狼才鲸
* \date 2022-11-08
* \note File format: UTF-8
******************************************************************************/
/************************************ 头文件 **********************************/
#include <stdio.h>
/************************************ 宏定义 **********************************/
#define UNUSED_VARIABLE(X) ((void)(X))
#define ITM_PORT8(n) (*(volatile unsigned char *)(0xe0000000 + 4*(n)))
#define ITM_PORT16(n) (*(volatile unsigned short *)(0xe0000000 + 4*(n)))
#define ITM_PORT32(n) (*(volatile unsigned long *)(0xe0000000 + 4*(n)))
#define DEMCR (*(volatile unsigned long *)(0xE000EDFC))
#define TRCENA 0X01000000
/*********************************** 接口函数 *********************************/
int fputc(int ch, FILE *f)
{
UNUSED_VARIABLE(f);
if(DEMCR & TRCENA) {
while(ITM_PORT32(0) == 0);
ITM_PORT8(0) = (char)ch;
}
return ch;
}
int main()
{
printf("Hello world!\n");
return 0;
}
- 参考网址:
linux 汇编 preserve8,为什么ARM汇编程序前要加PRESERVE8
ARM/THUMB汇编(补丁开发类)基础教程
Thumb 指令集
keil 环境下 AREA SUN ,NOINIT, READONLY , ALIGN=3 分别表示什么意思
【ARM汇编】SPACE和DCD指令的区别
STM32 stack_size和heap_size大小设置的意义
堆(heap)和栈(stack)有什么区别??
arm汇编指令-DCD
ARM 汇编 伪指令 MACRO及MEND
灵活使用ARM汇编的WEAK关键字
ARM汇编中B .的实际意义
【cm-3】汇编中的句子:AREA |.text|, CODE, READONLY, ALIGN=2详解
ARM汇编:汇编中proc、endp、ret、near、far指令用法
ARM Thumb指令集完整列表