ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

单片机启动流程详解:从复位向量到main函数的完整链路

单片机启动流程详解:从复位向量到main函数的完整链路 别急着研究那些花里胡哨的外设驱动先想一个最基础的问题你给单片机烧录完程序按一下复位键电源“咔哒”上电这个时候芯片内部到底发生了什么它凭什么就知道要去执行你写在 main 里的逻辑以前带过不少新人很多人一上来就写 while(1) 点灯问起来“单片机怎么运行的”就一脸茫然只肯抛出“不就跑 main 了嘛”这几个字。真的没这么简单。从你按下电源开关那一刻到 main 函数真正执行第一行 C 语句中间隔着复位、向量表、启动文件、C 运行时初始化、堆栈准备等一系列步骤。任何一个环节出问题表现就是各种各样的“灵异事件”代码烧进去没反应、全局变量初值不对、函数指针跳飞、又或者串口莫名其妙打出一堆乱码。这篇文章就从底层开始把这套流程从头到尾捋一遍。我会尽量用大白话把那些你经常看到但可能没深究的术语——比如向量表、Reset_Handler、栈顶地址、.data 段、.bss 段、链接脚本、SystemInit 等等——一件件讲透最后再附上一套实际排查流程。无论你是刚入门的单片机学习者还是已经工作两年但没认真啃过启动细节的工程师看完应该都会有收获。1. 上电那一瞬间芯片到底在干嘛1.1 别把复位当成“从头开始”很多人对复位的理解就是“程序重新跑一遍”这样理解不能说全错但会漏掉一个关键点复位不是 C 语言层面的“重新开始”而是硬件层面的一个强制归位操作。上电复位的瞬间单片机内部所有的寄存器都会被硬件重置成默认值。最关键的几个寄存器PC程序计数器被设置为复位向量指向的地址SP栈指针被设置为栈顶地址各种状态寄存器、控制寄存器恢复默认状态这里就牵出第一个术语——复位向量Reset Vector。它不是一个“按钮”或者“标志位”而是一个具体的地址。CPU 在上电复位后做的第一件事就是把 PC 的值设置成这个复位向量然后从这个地址取第一条指令。你可以把单片机想象成一个刚睡醒的人他不是先想“我今天要干嘛”而是先本能地看向一个固定的地方比如床头柜上的便签纸上面写着今天要做的第一件事。复位向量就是那张便签纸。1.2 内核不同起跑线就不同这里需要分清楚51内核、AVR 内核和 ARM Cortex-M 内核的启动机制是有差异的。很多人学完 51 再学 STM32 会很不适应很大程度就是因为这个起跑线逻辑变了。51 单片机复位后 PC 直接指向 0x0000这里的代码一般是一条跳转指令跳到真正的程序开始位置。它的向量表结构很简单每个中断一个入口按固定地址排列。AVR 单片机类似复位向量也在 Flash 起始地址通常是 0x0000 或 0x0001取决于具体型号的设置。ARM Cortex-M 系列比如 STM32、GD32这里要注意第一款地址不是放“跳转指令”而是放栈顶地址第二个字才是复位向量。也就是说CPU 上电后先读地址 0x00000000 处的值给 SP再读地址 0x00000004 处的值给 PC。这个差异真的很重要。我见过有 51 转 STM32 的工程师自己写链接脚本的时候还习惯性地把第一条指令放在地址 0 的地方结果程序根本跑不起来因为纯 51 时代的思维是“地址 0 放第一条指令”而 Cortex-M 的思维是“地址 0 放栈顶地址 4 放复位向量”。如果你用的是 STM32 这类芯片打开它的启动文件startup_xxx.s第一行通常会看到类似这样的内容; 向量表起点入口地址被链接器重定位到Flash首地址 __initial_sp DCD 0x20005000 ; 栈顶地址由链接脚本决定 DCD Reset_Handler ; 复位处理函数地址也就是说这张便签纸上写的不是“跳转指令”而是“栈顶地址”和“复位处理器地址”。ARM 的硬件设计者认为这样可以顺便把 SP 也初始化了一举两得。1.3 复位类型的细微差别才是不稳定的根源上电复位只是其中一种复位。做项目的时候你会遇到更多“莫名其妙重启”的情况这时候就得判断到底是哪种复位上电复位Power-On Reset, POR掉电再上电最常见外部复位NRST引脚拉低按键复位或外部看门狗复位窗口看门狗复位WWDG Reset程序跑飞或死循环时触发独立看门狗复位IWDG Reset另一种看门狗软件复位Software Reset程序自己触发这类复位之间的区别在 Cortex-M 芯片中通常可以通过 RCC 控制寄存器里的复位标志位来判断。排查“为什么单片机老是重启”时第一件事往往是读这个标志位看看到底是掉电了还是看门狗饿了。我实际排查过一个现场设备反复重启的问题代码逻辑看起来完全没问题后来才发现是程序主循环一趟跑太久超过了 IWDG 的喂狗周期被看门狗强杀复位了。这种问题如果搞不清复位类型的差异光盯着 C 代码看是永远找不到原因的。2. 启动文件程序员和硬件之间的“交接班”2.1 启动文件的第一张王牌栈顶地址前面提过Cortex-M 芯片 0x00000000 地址放的是栈顶地址。但为什么必须是栈顶地址而不是某个函数地址这要从“栈”的作用说起。栈Stack是程序运行时的临时存储区用来保存局部变量、函数调用的返回地址、函数参数等。每次调用一个函数CPU 都要把当前状态压到栈里等函数返回再弹出来就像抽屉一样一进一出。所以CPU 在运行任何 C 代码之前必须先知道“栈在哪、有多大”。如果你没设置好栈顶地址或者设置的地址超出 RAM 范围那么函数调用一旦开始数据就会写到未知区域轻则变量值被冲掉重则直接跑飞。来看看 STM32F103 启动文件开头的典型写法__initial_sp DCD 0x20005000这里的 0x20005000 一般不是随便写的而是由链接脚本算出来的。它代表 RAM 区域的最高可寻址地址常见习惯是把栈放在 RAM 最高处往下增长。换句话说编译器帮你算好了“栈顶在这个位置”你需要保证这个位置的合法性。有个最简单的类比你在一个小房间里堆杂物先是堆到天花板栈顶然后一层一层往下放。如果天花板高度算错了东西就会堆到楼上去——也就是写到其他内存区域去了。如果你在 MDK 里勾了 Use Memory Layout from Target Dialog那么这个栈顶地址其实是通过 Target 标签页里的 IRAM 起始地址加大小算出来的如果你用的是 GCC那么它由链接脚本的__stack等符号决定。2.2 中断向量表一张管理所有中断的“电话本”向量表Vector Table在 ARM 世界里不仅仅包含复位向量还包括所有中断服务函数的入口地址。从严谨角度讲它是一个“字数组”第 0 项是初始 SP第 1 项是 Reset_Handler后面依次是 NMI、HardFault、MemManage、BusFault、UsageFault……然后才是外设中断EXTI、TIM、UART、I2C 等。启动文件会按固定顺序定义这些中断的弱符号Weak Symbol并全部指向一个默认的Default_Handler。如果实际代码里定义了同名的中断函数链接器就会用真实函数覆盖这个弱符号。这也是为什么你写了 USART1_IRQHandler 之后中断确实能跳过去的原因。写启动文件的时候最怕的就是向量表漏项或者顺序对错。曾经有人把 USART1_IRQHandler 写成了 USART2_IRQHandler 的位置结果中断一直进不去因为发生 USART1 中断时CPU 直接跳到向量表里对应 USART2 的地址——如果你的代码没定义 USART2_中断函数就跳到了 Default_Handler 里的某个死循环甚至直接 HardFault。另外Cortex-M 的向量表还有一个特性它不一定非要放在 0 地址。通过设置 VTOR向量表偏移寄存器你可以把向量表挪到 RAM 或者其他 Flash 位置。这在做 Bootloader 和 OTA 的时候特别重要因为 APP 程序通常被烧录在某个偏移地址比如 0x08010000那么向量表也要跟着偏移到 0x08010000。很多人 Bootloader 跳转到 App 之后进不了中断就是忘了修改 VTOR。2.3 Reset_Handler 的主要任务不是马上跑 main当 CPU 完成复位向量取指后它会跳到 Reset_Handler 执行。这个函数的汇编代码通常非常短但包含几个关键步骤拷贝 .data 段已初始化全局变量从 Flash 到 RAM清零 .bss 段未初始化/零初始化全局变量设置堆指针如果需要调用 SystemInit用于配置时钟等系统初始化跳转到 C 库入口如 __main 或 main先别管这些步骤的细节你只需要抓住整体流程——启动文件干的事就是把“硬件能跑”变成“C 环境能跑”。很多工程师以为 Reset_Handler 一执行就马上跑到 main 了结果在中断里发现某个全局数组的值是随机的或者在 main 之前就发现串口乱码。其实都是因为不理解这部分时序。我们拿一个典型的 startup_stm32f103xe.s 片段来开刀Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT __main IMPORT SystemInit LDR R0, SystemInit BLX R0 LDR R0, __main BX R0 ENDP你看这段代码其实就做两件事先调用 SystemInit再跳到 __main。.data 和 .bss 的初始化是 __main 里面再去做的在 MDK 环境下或者更准确地讲是 C 库函数完成的。2.4 为什么会有弱符号WEAK这种设计启动文件里几乎所有中断函数都被标记为 [WEAK]意思是“弱符号”。如果你在应用代码里定义一个同名的强符号函数链接器会优先使用你的。这个设计让厂家提供的默认中断处理函数只起“备胎”作用而你的应用代码不受链接错误影响。但有时这个设计也会坑人。比如你在某个头文件里声明了一个中断处理函数但函数名拼写错了比如void TIM2_IRQHandler(void)写成了void TIM_IRQHandler(void)。因为启动文件里那个默认的弱中断处理函数是存在的链接器不会报错程序也能编译通过但中断触发后就进不了你的真实处理函数而是跑到默认的无限循环里。这种 bug 在嵌入式领域真的不少排查起来要多看向量表和 map 文件。3. C 运行时初始化给变量安排一个“合法身份”3.1 全局变量不是天生就有值的很多人在 C 课上写过这样的代码int counter 100; uint8_t buffer[256] {0};然后理所当然地认为程序一上电counter 就一定是 100buffer 里就全是 0。这在 PC 上确实基本如此但在单片机上如果不做处理这俩变量的初始值完全可能是上电后 RAM 里残留的随机值。道理很简单变量定义在 RAM 里RAM 上电后是不知道里面要放什么内容的。这时候就要引出编译链接阶段的“段”Section概念代码段.text存放程序代码一般在 Flash只读数据段.rodata存放 const 常量一般在 Flash数据段.data存放“已初始化且非零”的全局变量初始值在 Flash 里保存但运行时要拷贝到 RAM 里BSS 段.bss存放“未初始化或初始化为 0”的全局变量运行时要清零不占用 Flash 存储空间堆Heap给 malloc 等动态内存使用栈Stack函数调用和局部变量使用向下增长为什么 .data 会“一半在 Flash一半在 RAM”因为全局变量必须能在程序运行过程中被修改所以它必须待在 RAM 里但它的初始值是编译时就决定的比如 counter 100 这个“100”是固定不变的只能先放在 Flash 里。启动流程里就需要把 Flash 中存着的“100”拷贝到 RAM 中 counter 所在的位置。.bss 段就更省事了因为初值是 0不需要在 Flash 里额外占空间存 0只要在启动时把 RAM 中对应区域“填 0”就行。我之前见过有人把 1MB 的 uint8_t 数组直接定义为全局变量导致编译出来的固件体积异常大。为什么因为编译器可能把它放到了 .data 段数组初始值在 Flash 里全零保存了整整 1MB。如果放到 .bss 段Flash 就能省下这 1MB。这类细节搞懂之后你写代码时对全局变量声明位置的选择就会更有数。3.2 谁在做“拷贝 .data”和“清零 .bss”这件事在 MDK/ARMCC 环境下编译器会自动生成一段__main相关的启动代码负责初始化运行库包括把 .data 从 Load Region 拷贝到 Execution Region把 .bss 清零初始化堆调用__rt_entry最终再进入main在 GCC/ARM 环境下对应的通常是 crt0 或 GCCT 的 start 文件也会执行相似流程。但这里有个很微妙的点如果你的链接脚本烧录地址和加载地址不一致比如程序分成了 Boot 和 App 两个区域拷贝 .data 的源地址就不是“Flash 起始地址 0x08000000”而是要按链接脚本中定义的LOADADDR去取。我实际遇到过一个场景Bootloader 跳转到 App 后App 里有几个全局变量初始值不对排查半天才发现是链接脚本中 App 的加载地址Load Region设置错了一位启动代码去 Flash 里拷贝 .data 时读错了位置。如果你想验证这步工作是否正常最简单的办法是在 main 函数第一行打上断点然后通过调试器查看某个已初始化的全局变量的值。如果值是 0 或随机数基本可以断定 .data 拷贝没成功执行或者链接脚本有问题。3.3 链接脚本决定“变量去哪住”的总规划师启动文件能正常拷贝 .data、清 .bss背后依赖的是链接脚本。链接脚本的英文名可能是.ld、.icf、.sct或者 scatter 文件它定义了各段在 Flash 和 RAM 中的布局。举个简单的 GCC 风格链接脚本片段简化版MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 128K RAM (rwx) : ORIGIN 0x20000000, LENGTH 20K } SECTIONS { .text : { *(.isr_vector) *(.text*) *(.rodata*) } FLASH _sidata LOADADDR(.data); .data : { _sdata .; *(.data*) _edata .; } RAM AT FLASH .bss : { _sbss .; *(.bss*) *(COMMON) _ebss .; } RAM }这里最核心的概念是AT FLASH.data段的运行地址Execution Address在 RAM 里但加载地址Load Address在 Flash 里。正是因为有了这两个地址启动代码才知道“从哪里拷到哪里”。如果你改过链接脚本一定要特别注意_sidata、_sdata、_edata这些符号。它们由链接器生成被启动文件引用。如果改错了脚本链接器可能不会直接报错但程序运行过后就会出现奇奇怪怪的问题——最典型的就是“全局变量初始值诡异”。顺便说一句STM32 官方的 .icf 文件IAR 平台也包含类似的逻辑只是写法上略有不同。IAR 提供place in RAM等指令但最终目的一样。3.4 堆Heap没配好malloc 就是定时炸弹C 运行时初始化里还有一项容易被忽略堆的初始化。如果你在项目里用了malloc或者 C 的new而启动文件里堆的大小设置为 0 或特别小那么申请一小块内存都可能失败返回 NULL。在 MCU 开发中更推荐使用静态分配或者线程安全的内存池但有些情况下确实需要动态内存。在 MDK 启动文件里你会看到类似这样的定义Heap_Size EQU 0x00000200 AREA HEAP, NOINIT, READWRITE __heap_base Heap_Mem SPACE Heap_Size __heap_limit这里的__heap_base和__heap_limit是 C 库用来管理堆的锚点。如果你把 Heap_Size 改为 0而代码里又调用了malloc那程序多半会在某次申请时返回 NULL然后你的逻辑里如果没有判断就会一直操作 NULL 指向的数据最终死机或 HardFault。关于栈和堆的关系有一条不成文的经验能静态分配就静态分配能不用 malloc 就不用 malloc。嵌入式系统的内存既宝贵又紧张过度依赖动态分配机制出问题时真的很难排查。4. 从 Reset_Handler 到 main中间还要过几道关4.1 SystemInit不给时钟CPU 连“心跳”都没有在跳到 __main 或 main 之前很多芯片要求先调用一个叫 SystemInit 的函数。它的主要作用是配置系统时钟包括选择时钟源内部 RC 还是外部晶振、配置 PLL 倍频、设置总线分频器等。你可能会有个疑问为什么时钟配置要放在 main 之前因为 C 代码运行需要准确的时间基准。如果你要初始化外设 UART而系统时钟还没配好波特率计算出来就是错的串口通信自然失败。以 STM32F103 为例SystemInit 函数里会判断外部高速晶振是否正常正常则切换到 PLL 倍频后的时钟例如 72MHz否则使用内部高速时钟HSI 8MHz。这个逻辑代码很复杂一般是芯片厂商提供的库生成的并不需要你手动去阅读每一行但是你要知道它为什么存在于 main 之前。现实中不少人做过这样的事在 main 里第一句话写了while(1)然后逐步初始化硬件但忘了检查 SystemInit 是否正常导致芯片实际工作频率只有默认的 8MHz而所有外设都是按 72MHz 算的。结果串口乱码、定时器时间不对折腾半天才发现是主频不对。4.2 __main 与 main 的区别你以为的入口其实还有前置这里必须把 MDK/ARMCC 环境下__main和main的关系说清楚__main不是我们写的那个 C 语言 main 函数它是 C 库的启动入口函数。它会调用__scatterload完成代码和数据拷贝调用__rt_entry完成运行库初始化最后才去调用你写的main。所以当你打开反汇编看到程序跳到__main而不是main时不要惊讶。这步是正常的。GCC 环境下也有类似逻辑只不过入口通常叫_start然后依次调用__libc_init_array初始化全局构造函数、__libc_init等最后才进入main。如果你用 C 写嵌入式代码注意还有一层“全局对象构造函数”的调用阶段。有一些人明明写了static SomeClass obj;但调试时发现构造函数没执行大多就是因为链接选项里把__libc_init_array这类初始化函数给丢了或者使用了不支持全局构造的启动文件。4.3 为什么有时候“main 第一行设了个局部变量”就死机这个问题很经典也非常容易踩坑。例如int main(void) { uint8_t bigBuffer[1024]; // ... }如果这个函数里还用到了太多局部变量而这些局部变量需要的内存超过了当前栈剩余空间那就直接覆盖到其他区域轻则数据错误重则 HardFault。初始化栈顶地址是启动文件早就做好的如果栈大小定义太小函数嵌套层数又深那么栈溢出只是时间问题。很多人用调试器看到 PC 卡死在 HardFault_Handler 里第一反应是“是不是野指针”查了半天才发现只是栈不够。我调试过一个项目现象是某个函数调用一返回另外一个模块里的全局数组内容就变了。后来发现栈顶地址和那个全局数组在 RAM 里非常接近函数调用一深栈溢出就把数组内容冲了。这种问题非常坑因为表现看起来特别像普通的内存越界。经验做法在启动文件里将栈大小留足余量。工程实践通常为所有任务的栈、中断栈、以及 main 函数栈统一规划尽量用静态数组分给任务而不是依赖一个巨大的 main 栈。4.4 中断向量表偏移Bootloader 跳转失败的高频元凶如果你之前做过 OTA 或者 Bootloader一定知道SCB-VTOR FLASH_BASE | OFFSET这种操作。这行代码把中断向量表从默认的 0x08000000 偏移到应用程序所在的基地址比如 0x08010000。这里有个容易被忽略的顺序必须确保 App 的向量表本身就在 Flash 偏移位置。如果你在编译 App 时没有设置链接地址偏移比如 MDK 里的 IROM1 起始地址而是让 App 也编到 0x08000000那么即使运行时去修改 VTOR 也没有用因为向量表在物理上根本没有排到对应位置。我之前写过一个小实验利用 bootloader 跳转到 App然后点一个按键触发外部中断。跳转是很成功的因为 main 函数里的点灯逻辑已经跑起来了但按键中断死活不触发。单步调试发现PC 在中断触发后跑到了默认向量表里的错误位置一查才发现 App 工程的 IROM1 起始地址没改向量表重叠在 Boot 区域。这个坑在 Bootloader 开发中属于高频问题比代码本身的逻辑错误还要常见。要解决它最直接的方式就是用调试器查看 VTOR 的值以及向量表所在内存区域的前 8 个字节是否和预期一致。5. 上手验证自己动手看启动流程5.1 用调试器一步步观察 PC 和 SP纸上得来终觉浅最好自己动手验证一下。这里以 STM32 和 Keil MDK 为例但道理通用于所有 Arm Cortex-M 芯片。打开你的工程连接调试器按 F10 单步执行之前先确认这几个寄存器SP复位时应该等于你启动文件里定义的栈顶地址PC复位时应该等于 Reset_Handler 的地址VTOR通常为 Flash 基地址比如 0x08000000然后单步执行一两步你会看到 PC 先在 SystemInit 函数里跑一段然后跳到一个奇怪的名字__main或者__scatterload最后才进入main。整个过程快的话只有几十条汇编指令但每一条都有它的存在价值。5.2 反汇编怎么看“拷贝 .data”和“清零 .bss”如果你用的调试器支持查看反汇编直接把 PC 停在 Reset_Handler 或 __main 入口看看汇编代码。以 GCC 工具链为例反汇编后你会在_start或某个 crt 文件中看到类似这样的循环ldr r0, _sidata ldr r1, _sdata ldr r2, _edata copy_loop: ldr r3, [r0], #4 str r3, [r1], #4 cmp r1, r2 bcc copy_loop这段代码就是在做 .data 段拷贝。如果你看到_sidata、_sdata、_edata的值与链接脚本中的布局一致说明拷贝逻辑正常。清 .bss 的汇编也很类似区别只是从某个地址开始写 0写到一个结束地址为止。5.3 最小验证实验自己写一个超级简单的空项目为了彻底感受启动流程建议你做一个最小实验新建一个工程不调用厂商库只用汇编写一个 startup 文件startup 文件里只定义栈顶、复位向量、以及一个简单的 Reset_HandlerReset_Handler 里只做一件事初始化几个全局变量区域然后跳转到 mainmain 里点亮一个 LED这个过程会让你意识到原来平时编译器悄悄做的事情每一步都是可见的。下面给一个 GCC ARM CM3 的最小启动文件示例伪完整版足以跑通没有复杂依赖的环境.syntax unified .cpu cortex-m3 .thumb .global _start .global main .section .isr_vector,a,%progbits .word _estack /* 栈顶地址 */ .word _start /* 复位向量 */ .word Default_Handler /* NMI */ .word Default_Handler /* HardFault */ /* 其余中断向量按需添加 */ .section .text .thumb_func _start: /* 1. 拷贝 .data */ ldr r0, _sidata ldr r1, _sdata ldr r2, _edata copy_loop: cmp r1, r2 bge clear_bss ldr r3, [r0], #4 str r3, [r1], #4 b copy_loop clear_bss: ldr r1, _sbss ldr r2, _ebss movs r3, #0 bss_loop: cmp r1, r2 bge init_done str r3, [r1], #4 b bss_loop init_done: bl main b . Default_Handler: b .看见没有一个最简单、最纯粹的启动流程其实就这几十行汇编。虽然你的工程通常不需要自己手写但一旦你理解了这段逻辑再去看厂家提供的 startup 文件就不会再有任何心理障碍了。5.4 实操环节如何判断“卡在启动还是卡在 main”遇到“程序没反应”的问题第一件事就是判断程序跑到了哪里。方法其实很简单在 main 第一行打断点看程序是否能停在断点上如果能停在 main说明启动流程是通的问题在外设配置或业务逻辑如果不能停在 main那就逐步检查启动流程先看 PC 是否在 Reset_Handler再看 SP 是否正常再看时钟初始化有没有卡死有些芯片的启动流程里有一段“等待外部晶振稳定”的代码如果 PCB 上晶振焊接不良或者起振电容选得离谱程序很可能在 SystemInit 阶段就永远卡在 while 循环里等待 OSC_RDY。这个现象在调试上很有迷惑性因为你看代码逻辑没任何问题就是跑不过去。6. 我踩过的坑和一些个人的实操心得6.1 栈大小不是“越大越好”但千万别太小很多人认为反正 RAM 够大栈直接给到 8KB、16KB。但 RAM 是有限资源栈开得大全局变量和堆的空间就被挤占了。尤其对于那些需要跑 RTOS 的项目每一个任务都有自己的栈全部加起来要合理规划。我一般的习惯是main 函数栈给到 1KB~2KB 以上如果 main 里不放大数组中断栈和任务栈单独规划。如果代码里需要临时缓冲区优先考虑静态数组而不是在函数内部开大数组。6.2 别把 malloc 当主力在资源受限的 MCU 上malloc 是一把双刃剑。内存碎片一旦产生程序运行几个月后可能突然申请不到一块连续内存而系统还不会立刻告诉你哪里错了。最稳妥的方案是启动时一次分配使用一个内存池或固定长度数组来管理运行时不反复申请释放。如果非要用至少要保证启动文件里 Heap_Size 足够并且 malloc 之后记得判断返回是否为 NULL。6.3 硬件调试器能救你但别迷信它用调试器可以很方便地看 PC、SP、变量值但有些问题恰恰只在“全速运行”时出现一单步就好。比如看门狗超时、外部信号时序、或者某个临界区竞争这些都不是启动文件本身的问题而是业务代码的问题。如果程序在全速运行时有异常在单步时又正常可以优先考虑是否在启动流程阶段有一个延时依赖或者看门狗在 SystemInit 还没跑完时就开始计数了。有些芯片的独立看门狗一旦开启就无法用软件关闭必须在启动早期就及时喂狗否则程序永远会在 Reset_Handler 附近反复复位。6.4 给新手的三条建议第一遇到程序跑不了的现场不要急着怀疑编译器和芯片先确认“上电到 main 之间的路径是否畅通”。你可以把 main 第一句改成简单的置位一个 GPIO 输出高电平并接一个 LED 或示波器能直观证明程序跑到了哪个阶段。第二多翻启动文件和链接脚本。厂家提供的 startup 文件和链接脚本大多是模板但它们几乎包含了所有你想知道的细节。看明白这俩文件你对整段程序的控制力会直接上一个台阶。第三任何时候改内存布局、中断向量、栈大小都要保持谨慎改完务必做一次基础的回归测试。启动过程是地基地基只要歪了一点点上层代码写得再好看也没用。也是因为想通了这些我现在看到“程序烧进去没反应”第一反应从来都不是改业务逻辑而是先确认复位向量和栈顶地址那两个关键值是否和预期一致。搞单片机越底层的事越值得花时间弄明白。
返回列表