建立一个空白的工程,想用和我同样开发环境的可以去看《使用VSCode+EIDE+GNU开发MCU》,不想用你平时怎么搭建工程保持就可以。进入main函数把cubemx自动生成的时钟配置,初始化函数,注释掉暂时先不用。

这里我想先说一下<stdint.h>头文件这是C99引入的,核心目标是标准化整数的位宽,避免因不同硬件架构或编译器导致的整数类型大小差异问题比如int在不同平台可能是4字节或8字节,使用<stdint.h>所有的整数表示都是uint32_t,int8_t这种形式,明确了位宽和有无符号,确保代码在不同平台编译行为一致。
#ifndef _INT16_T_DECLARED
typedef __int16_t int16_t ;
#define _INT16_T_DECLARED
#endif你去查看int16_t的定义发现其实就是使用typedef进行的重定义并没有什么秘密,你也可以自己进行重定义,建立一个自己的标准,但我觉得在已经有一个标准的情况下这么做没有意义。
声明一个有符号32位整数并初始化为0,之后不断自增,记得不要开启编译器优化,不然counter变量在之后没有被用到,编译后相关代码将被优化掉。
int main(void) {
/* USER CODE BEGIN 1 */
int32_t counter = 0;
counter++;
counter++;
counter++;
counter++;
······
counter++;
while (1) {
}
}下面是对应的汇编代码,注意你的编译器行为可能和我这里不一致,有的不会把sp复制到r7寄存器中而是直接使用,有的直接在寄存器持续自加没有从内存中读取的操作,我使用的编译器显然在这个级别优化下做了一些复杂的工作。
8000228: af00 add r7, sp, #0
/*将栈顶指针复制到r7寄存器*/
int32_t counter = 0;
800022a: 2300 movs r3, #0
800022c: 607b str r3, [r7, #4]
/*将 0 移动到寄存器 r3 中,将寄存器 r3 中的值(即 0)存储到内存地址 r7 + 4,完成初始化*/
counter++;
800022e: 687b ldr r3, [r7, #4]
8000230: 3301 adds r3, #1
8000232: 607b str r3, [r7, #4]
/*从栈地址 sp + 4 处加载 counter 的值到寄存器 r3,
将 1 加到寄存器 r3 中的值上,
将寄存器 r3 中的新值存储回栈地址 sp + 4
之后的每次自加都会执行相同汇编代码*/
在调试状态下每次单步执行都可以观察到counter+1,符合预期行为,这里已经运行到编写C代码最后一次自加,之后就要进入死循环,我在这里打了断点让程序停住,想要继续自增可以手动修改pc寄存器,修改pc寄存器为第一次自增的指令地址:0x0800022e,并且一并修改了counter计数值为0x7ffffff9。

之后再次单步运行程序会从第一个counter自增语句开始执行,当然这没有什么实际意义,但是我觉得你还是可以试着动手修改一次pc寄存器改变程序正常执行流程,这也是一个重要的调试技巧。
之后继续单步执行几次后如果你把视图切换成十进制你会发现counter变成了负数,这牵扯到了计算机内不同类型整数的范围问题,这在嵌入式编程中是一个尤其需要注意的问题,要防止计数溢出。

这里我贴上了32位有符号的计数“循环”,其他不同类型也各自有自己的“循环”,我这里不一一列举,有兴趣你可以自己尝试,记住不同类型的范围有助于你编程声明变量时选择合适类型既不会溢出也不会分配过大的空间导致RAM浪费。

上面counter不断自增的过程其实就是一个最常见的顺序结构,最后我通过修改pc指针强行改变了正常的执行流程,但是我们不可能在程序正常运行时进入调试状态更改pc寄存器,顺序结构显然不能满足我们复杂的程序要求,其实用C语言可以很简单的实现循环和选择结构,这里我在if-else结构里都进行了一次加常数操作没有什么特殊意义,只是为了防止什么都不做被编译器优化掉,你写程序的的时候应该在里面做些有意义的事。
int main(void) {
/* USER CODE BEGIN 1 */
int32_t counter = 0;
while (counter < 21) {
++counter;
if ((counter & 1) != 0) {
counter=counter+4;
/* do something when the counter is odd */
}
else {
counter=counter+2;
/* do something when the counter is even */
}
}
while (1) {
}
}涉及到指令跳转apsr寄存器就必不可少,其中的状态位是进行跳转的依据,cmp实际上是进行了一个减法操作,只是更新了状态标志位但计算结果不进行保存。
| N(负数) | Z(零) | C(进位) | V(溢出) | Q(饱和) | ······ |
|---|---|---|---|---|---|
| [31] | [30] | [29] | [28] | [27] | [26:0] |
800022e: ,----- e00e b.n 800024e <main+0x2a> /*无条件跳转*/
8000230: ,--|----> 687b ldr r3, [r7, #4] /*加载counter到r3*/
8000232: | | 3301 adds r3, #1 /*r3+1*/
8000234: | | 607b str r3, [r7, #4] /*r3写入堆栈*/
8000236: | | 687b ldr r3, [r7, #4] /*加载counter到r3*/
8000238: | | f003 0301 and.w r3, r3, #1 /*r3中的值与立即数#1进行按位与操作,并将结果存储回r3*/
800023c: | | 2b00 cmp r3, #0 /*r3-0,*/
800023e: | | ,-- d003 beq.n 8000248 <main+0x24> /*Z==1时跳转*/
8000240: | | | 687b ldr r3, [r7, #4] /*加载counter到r3*/
8000242: | | | 3302 adds r3, #4 /*r3+4*/
8000244: | | | 607b str r3, [r7, #4] /*r3写入堆栈*/
8000246: | +--|-- e002 b.n 800024e <main+0x2a> /*无条件跳转*/
8000248: | | '-> 687b ldr r3, [r7, #4] /*加载counter到r3*/
800024a: | | 3302 adds r3, #2 /*r3+2*/
800024c: | | 607b str r3, [r7, #4] /*r3写入堆栈*/
800024e: | '----> 687b ldr r3, [r7, #4] /*加载counter到r3*/
8000250: | 2b14 cmp r3, #20 /*r3-20,*/
8000252: '-------- dded ble.n 8000230 <main+0xc> /*Z==1 || N!=V 时跳转*/
8000254: ······条件分支指令都是由b+xx组成,它们会更改pc寄存器,分析这些指令可以让你更了解ARM Cortex-M 处理器内部工作原理,简单来说条件分支指令会根据apsr寄存器数值判断是否需要跳转,如果需要更改pc寄存器。
这里我把各种情况给贴上,但你并不需要记忆。我相信编译器非常智能,大多数时候比你要更了解处理器,并不需要你每次剖析指令,而且开启高等级优化之后分析变得很困难,这里我也只是说一下处理器是如何进行跳转的,之后非必要我也不会分析指令,把时间留给编写代码更有意义。

评论已关闭