嵌入式启航 嵌入式启航
首页 作品展示 个人主页
首页 作品展示 个人主页
binarybard
binarybard
嵌入式软件工程师,专注于MCU+RTOS技术栈,了解各种MCU底层。
binarybard
binarybard
嵌入式软件工程师,专注于MCU+RTOS技术栈,了解各种MCU底层。

分类

  • 默认分类 1
  • 开发环境 1
  • keil_C51 5
  • 嵌入式C语言 3
  • 常用算法 1

最新文章

  • CRC校验
    2026-02-07
  • C语言面向对象
    2026-01-31
  • 计数与控制结构
    2026-01-24
  • 控制外部世界
    2026-01-17
  • 51单片机函数调用与可重入函数
    2026-01-10

计数与控制结构

binarybard 2026年01月24日 嵌入式C语言 0 条评论

建立一个空白的工程,想用和我同样开发环境的可以去看《使用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寄存器。

这里我把各种情况给贴上,但你并不需要记忆。我相信编译器非常智能,大多数时候比你要更了解处理器,并不需要你每次剖析指令,而且开启高等级优化之后分析变得很困难,这里我也只是说一下处理器是如何进行跳转的,之后非必要我也不会分析指令,把时间留给编写代码更有意义。

上一篇
控制外部世界
下一篇
C语言面向对象

评论已关闭

© 2025-2026 嵌入式启航.
豫ICP备2025159703号    备案图标 豫公网安备41172102000273号