ARTICLE DETAIL

资讯详情

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

单片机上电启动全过程:从复位向量到main函数的硬件-软件协同

单片机上电启动全过程:从复位向量到main函数的硬件-软件协同 1. 这不是“上电就跑main”的童话而是一场精密的硬件-软件协同仪式你手里的那块STM32开发板或者那块贴着散热片的STC8H甚至是你焊在洞洞板上的经典AT89C51——当手指按下电源开关LED灯亮起串口打印出“Hello World”你有没有想过从电池电压跌落到0V的彻底断电状态到main函数第一行代码开始执行这中间到底发生了什么不是编译器文档里轻描淡写的“复位后跳转到Reset_Handler”也不是教科书上一笔带过的“程序从0x08000000开始运行”。这是一段被层层封装、刻意隐藏、却决定整个系统生死存亡的“暗黑旅程”。我干单片机这行十多年从51时代用示波器抓复位信号到STM32时代用J-Link Trace看指令流再到现在给国产RISC-V芯片写裸机启动代码踩过的坑比烧过的芯片还多。最常被问到的问题就是“为什么我的程序一上电就跑飞”、“为什么串口没反应但LED在闪”、“为什么调试器能连上但main函数就是不进去”——这些问题90%都卡在“从没电到跑main”这个看似简单的环节里。它不是一条直线而是一个由硬件电路、物理时序、汇编代码、链接脚本、C运行时库共同编织的严密链条。任何一个环节的微小偏差比如晶振起振慢了100ns或者向量表偏移地址算错了一个字节整个系统就会在启动的第10毫秒内无声无息地崩溃。这篇文章就是要把这条“暗黑旅程”彻底照亮。我们不讲抽象概念只讲真实发生的物理事件电源管理芯片如何给VDD拉高复位电路里的电容如何充电CPU核如何从复位状态退出Flash控制器如何被初始化向量表如何被CPU读取并跳转startup.s里的汇编指令如何逐条搬运数据、清零BSS段、设置堆栈指针C运行时库如何调用__main完成全局变量初始化最终才把控制权交到你写的main()函数手里。每一个步骤我都用实测波形、寄存器快照、反汇编代码和亲手调试的日志来佐证。这不是理论推演这是我在无数个凌晨三点对着逻辑分析仪和JTAG调试器一帧一帧抠出来的真相。如果你是刚学单片机的大一学生这篇文章会告诉你为什么你写的第一个“点亮LED”程序能成功背后有几十个你从未见过的隐性步骤在默默支撑如果你是做了三年项目的工程师这篇文章会帮你定位那些“偶尔复位失败”、“冷机启动异常”的顽疾根源如果你是正在移植Bootloader或写RTOS内核的开发者这篇文章就是你理解系统底层行为的基石。它不教你如何写一个功能模块而是教你如何让整个系统在每一次上电的瞬间都稳稳当当地、可预测地、百分之百地抵达那个你熟悉的main()函数入口。2. 启动全流程拆解从物理上电到C语言世界的第一声问候2.1 硬件层电源、复位与时钟——一切的物理起点“没电”不是一句空话它意味着所有晶体管的栅极电压为0所有寄存器内容为随机值所有总线处于高阻态。上电过程本质上是一场精密的电压爬升竞赛。首先电源管理芯片LDO或DC-DC开始工作。以常见的AMS1117为例其输入电压Vin从0V上升到5V输出Vout并不会立刻跟随。由于内部反馈环路和输出电容通常10μF的存在Vout的上升时间tRise由公式 tRise ≈ 2.2 × R × C 决定其中R是等效输出阻抗。实测中这个过程往往需要1~5ms。在这段时间内MCU的VDD引脚电压低于其最低工作阈值如STM32F103是2.0VCPU核、Flash、SRAM全部处于不可用状态。此时任何对寄存器的读写操作都是无效且危险的。紧接着是复位电路。绝大多数单片机采用低电平复位Active-Low Reset。一个典型的RC复位电路由一个10kΩ电阻和一个100nF电容组成。上电瞬间电容两端电压不能突变因此RESET引脚被拉低。随着电容通过电阻充电RESET引脚电压按指数曲线 V(t) Vcc × (1 - e^(-t/RC)) 上升。当V(t)超过复位释放阈值如0.7×Vcc时复位信号才被撤销。计算一下RC 10kΩ × 100nF 1ms达到0.7Vcc大约需要1ms。这意味着即使VDD已经稳定CPU也必须再等待至少1ms才能脱离复位态。我曾在一个项目中遇到冷机启动失败就是因为客户为了降低成本把复位电容换成了10nF导致复位时间不足CPU在Flash尚未准备好时就开始取指直接跑飞。最后是时钟系统。这是最容易被忽视的致命环节。以STM32为例上电后默认使用内部高速RC振荡器HSI8MHz但它的精度只有±1%且需要等待稳定。而如果你的代码配置了外部晶振HSE那么CPU必须先等待HSE就绪标志RCC_CR.HSERDY置位。这个等待不是“立刻就绪”而是需要一个“稳定时间”。根据ST官方手册HSE从起振到稳定典型时间为1~10ms。如果在HSE未稳定前就切换时钟源后果是灾难性的系统时钟频率错误UART波特率飘移定时器溢出时间不准甚至CPU指令周期紊乱。我在调试一个基于STM32L4的低功耗设备时发现它在低温环境下-20℃启动失败最终定位到是HSE在低温下起振时间延长至15ms而我的启动代码只等待了10ms导致后续所有外设初始化都建立在错误的时钟基础上。提示不要依赖“上电即稳定”的幻想。务必查阅你所用MCU的数据手册找到“Power-On Reset Timing”、“Reset Pulse Width”和“Oscillator Startup Time”三个关键参数并在你的启动代码中留足余量。一个安全的做法是在复位释放后强制延时20ms再开始任何时钟配置。2.2 CPU核层复位向量与指令获取——CPU的第一次“睁眼”当复位信号撤销CPU核从硬件复位状态退出。此时它做的第一件事不是执行你的C代码而是去一个固定的、由芯片厂商硬编码的地址读取一个32位的数值。这个地址就是复位向量Reset Vector。对于ARM Cortex-M系列STM32、GD32等这个地址是0x00000004。注意这不是程序的起始地址而是“复位中断服务程序入口地址”的存放位置。CPU会先从0x00000000读取初始堆栈指针MSP的值然后从0x00000004读取复位处理程序的入口地址。这两个值共同构成了向量表Vector Table的前两个元素。这个向量表通常被放置在Flash的起始位置0x08000000。但这里有个关键细节向量表的位置是可以重映射的。例如STM32可以通过设置SYSCFG_MEMRMP寄存器将向量表重映射到SRAM0x20000000或系统存储器0x1FFF0000。这意味着CPU在复位后会先去0x00000000读取MSP但这个0x00000000地址可能已经被映射到了Flash、SRAM或系统存储器。所以向量表的实际物理位置取决于芯片的启动模式Boot Mode和重映射配置。我曾在一个项目中因为误将BOOT0引脚接高导致芯片从系统存储器启动而我的向量表却放在Flash里结果CPU从0x1FFF0000读取到的是一堆FF直接跳到非法地址进入HardFault。一旦CPU读取到复位向量地址比如0x08000100它就会将PC程序计数器设置为该值并开始从那里取指执行。这个地址指向的就是startup.s文件中定义的Reset_Handler标号。此时CPU还处于“纯汇编世界”没有任何C语言环境堆栈指针SP刚刚被初始化为MSP所有通用寄存器R0-R12的值都是未知的。Reset_Handler就是这场启动仪式的司仪它要负责搭建起C语言世界所需的一切基础设施。2.3 汇编层startup.s——构建C世界的“脚手架”startup.s文件是连接硬件世界与C语言世界的唯一桥梁。它不是可有可无的模板而是每一行都关乎生死的精密指令。让我们以一个典型的STM32 startup_stm32f103xb.s为例逐行拆解.section .text.Reset_Handler .weak Reset_Handler .global Reset_Handler Reset_Handler: /* 1. 初始化堆栈指针 */ ldr sp, _estack /* 从链接脚本中加载栈顶地址 */ /* 2. 关闭全局中断 */ cpsid i /* Set PRIMASK to disable all interrupts */ /* 3. 初始化数据段 (.data) */ ldr r0, _sdata /* 加载.data段在RAM中的起始地址 */ ldr r1, _edata /* 加载.data段在RAM中的结束地址 */ ldr r2, _sidata /* 加载.data段在Flash中的起始地址即初始化值 */ movs r3, #0 /* 清零计数器 */ b LoopCopyDataInit /* 跳转到拷贝循环 */ CopyDataInit: ldrb r4, [r2, r3] /* 从Flash中读取一个字节 */ strb r4, [r0, r3] /* 写入RAM中对应位置 */ adds r3, r3, #1 /* 计数器1 */ LoopCopyDataInit: adds r3, r0, r3 /* 计算当前RAM地址 */ cmp r3, r1 /* 与.data段结束地址比较 */ ble CopyDataInit /* 如果未完成继续拷贝 */ /* 4. 清零BSS段 (.bss) */ ldr r0, _sbss /* BSS段起始地址 */ ldr r1, _ebss /* BSS段结束地址 */ movs r2, #0 /* 准备清零值 */ b LoopFillZerobss FillZerobss: strb r2, [r0], #1 /* 将r2(0)写入[r0]然后r0自增 */ cmp r0, r1 /* 比较当前地址与结束地址 */ ble FillZerobss /* 未完成则继续 */ /* 5. 调用C运行时初始化 */ bl __main /* 调用标准C库的__main函数 */ /* 6. 跳转到main函数 */ bx lr /* 返回此时__main已将控制权交给main */这段代码的核心任务有四个堆栈初始化ldr sp, _estack。_estack是一个符号由链接脚本linker script定义指向RAM的最高地址。这是C函数调用、局部变量分配、中断嵌套的基石。如果这个地址错了比如指向了未初始化的RAM区域那么第一次函数调用就会导致栈溢出系统崩溃。数据段拷贝.data段存放的是已初始化的全局/静态变量如int a 5;。这些变量的初始值必须存储在Flash中因为Flash是非易失性的但在程序运行时它们必须位于RAM中因为RAM可读写。startup.s的任务就是把Flash里存的初始值一字节一字节地拷贝到RAM的对应位置。这个过程必须精确拷贝长度由链接脚本生成的_sdata,_edata,_sidata三个符号决定。如果链接脚本配置错误比如把.data段的长度算少了那么部分全局变量就不会被正确初始化导致程序行为诡异。我曾调试一个Modbus通信程序发现寄存器值总是错乱最终发现是链接脚本里.data段的结束地址_edata被写错了导致一个关键的接收缓冲区没有被初始化里面全是随机值。BSS段清零.bss段存放的是未初始化的全局/静态变量如int b;。根据C标准它们必须被初始化为0。startup.s通过一个简单的循环将_sbss到_ebss之间的所有RAM字节写0。这个过程同样依赖于链接脚本提供的符号。如果_ebss地址超出了RAM范围清零操作就会写入非法地址引发BusFault。调用__main这是GNU ARM工具链arm-none-eabi-gcc的标准做法。__main是一个由C库提供的函数它会进一步调用__libc_init_array去执行所有被__attribute__((constructor))标记的函数以及完成其他C运行时的收尾工作。如果你使用的是Keil MDK它没有__main而是直接调用__rt_entry。这就是为什么不同编译器生成的startup.s文件看起来很像但关键跳转指令却不同。混淆它们会导致链接失败或启动失败。注意cpsid i指令关闭全局中断是为了确保在初始化关键数据结构如堆栈、数据段时不会被中断打断。这是一个原子操作必须在任何可能触发中断的代码之前执行。2.4 C运行时层__main与全局对象——C世界的正式开幕当startup.s执行完bl __main控制权就移交给了C运行时库。__main函数本身并不复杂它的核心工作是调用__libc_init_array。__libc_init_array会遍历一个特殊的数组——.init_array段。这个段由链接器自动收集所有带有constructor属性的函数地址。例如__attribute__((constructor)) void init_uart(void) { // 初始化串口 RCC-APB2ENR | RCC_APB2ENR_IOPAEN; GPIOA-CRH ~0xF0; GPIOA-CRH | 0x40; // ... 其他配置 }编译器会将init_uart的地址放入.init_array段。__libc_init_array会按顺序调用这个数组里的每一个函数。这是实现“自动初始化”的关键机制也是很多高级框架如CMSIS-RTOS的初始化入口。如果你在这个阶段初始化了某个外设而该外设的时钟尚未使能那么调用就会失败。紧接着__main会调用__do_global_ctors或类似函数用于调用C全局对象的构造函数。虽然单片机项目大多用C但如果你混用了C这个步骤就至关重要。一个未正确调用的构造函数可能导致一个全局对象的成员变量为随机值从而在main函数里第一次访问时引发故障。最后__main会调用main函数。此时所有的硬件时钟、GPIO、外设都已按你的代码配置完毕所有的全局变量都已被正确初始化.data已拷贝.bss已清零堆栈已就位中断系统也已准备好虽然startup.s里关了中断但你的main函数里通常会重新使能。这才是你真正熟悉的、可以放心写业务逻辑的C语言世界。从这一刻起“跑main”才真正开始。3. 核心技术点深度解析向量表、Reset_Handler与链接脚本的生死绑定3.1 向量表Vector TableCPU的“导航地图”错一位就全盘皆输向量表不是一段普通的内存它是CPU在复位、中断、异常发生时用来查找对应处理程序入口的“绝对权威”。它的结构是严格定义的每个元素的偏移量Offset都是固定的。以ARM Cortex-M3/M4为例向量表的前16个元素是系统异常从第17个开始才是用户定义的中断如USART1_IRQn, TIM2_IRQn。其布局如下偏移量 (Hex)名称描述0x00Initial Stack Pointer (MSP)复位时的主堆栈指针初始值0x04Reset Handler复位中断服务程序入口地址0x08NMI Handler不可屏蔽中断处理程序入口地址0x0CHardFault Handler硬件故障处理程序入口地址0x10MemManage Handler存储器管理故障处理程序入口地址0x14BusFault Handler总线故障处理程序入口地址0x18UsageFault Handler用法故障处理程序入口地址.........0x40SVCall Handler系统服务调用处理程序入口地址0x44Debug Monitor Handler调试监控处理程序入口地址0x48PendSV Handler可悬起系统调用处理程序入口地址0x4CSysTick Handler系统滴答定时器处理程序入口地址关键点在于CPU在复位时会无条件地、机械地从0x00000000和0x00000004读取这两个32位值。它不会检查这个地址是否有效也不会验证这个值是否指向一个合法的函数。如果0x00000000处的值是一个非法地址比如0xFFFFFFFF那么MSP就会被设置为一个无效值后续任何堆栈操作都会失败。如果0x00000004处的值是0x00000000那么CPU就会跳转到地址0x00000000去取指而那里很可能是一片空白导致进入HardFault。因此向量表的生成和放置是启动流程中最不容出错的一环。它由三部分共同决定链接脚本Linker Script定义了向量表在内存中的位置通常是ORIGIN 0x08000000和大小.vectors : { *(.vectors) }。startup.s文件定义了向量表的内容即.vectors段里具体存放哪些Handler的地址。编译器选项-T指定链接脚本-Wl,--defsym__Vectors0x08000000可以强制指定向量表起始地址。最常见的错误是“向量表偏移”。例如你修改了链接脚本将程序起始地址从0x08000000改为了0x08002000为了给Bootloader留空间但忘记在startup.s中更新向量表的放置位置。结果向量表被放在了0x08002000而CPU依然固执地从0x08000000读取读到的自然是垃圾数据。解决方法是在链接脚本中明确指定.vectors段的起始地址并确保它与CPU期望的地址一致。实操心得在Keil MDK中可以在“Options for Target” - “Utilities” - “Settings” - “Flash Download”里勾选“Verify Code Download”这样烧录后会自动校验Flash内容包括向量表。在STM32CubeIDE中可以在“Run Configurations” - “Startup” - “Load symbols and memory”里启用“Load executable file”和“Load symbols”这样调试时就能看到向量表的真实内容。3.2 Reset_Handler不只是一个标号而是启动流程的“总指挥”Reset_Handler的职责远不止“跳转到main”。它是一个高度定制化的、与具体硬件和项目需求强耦合的函数。在裸机开发中Reset_Handler通常包含以下可选但至关重要的扩展看门狗喂狗如果系统启用了独立看门狗IWDG或窗口看门狗WWDGReset_Handler的第一行就应该喂狗。否则在启动过程中如果喂狗超时看门狗会再次触发复位形成“复位风暴”。我曾在一个工业控制器项目中因为忘记在Reset_Handler里喂IWDG导致设备在上电后反复重启日志里只有一行“System Reset”排查了三天才发现是这个原因。Flash预取和缓存配置对于高性能MCU如STM32H7在执行任何代码前需要配置Flash的预取缓冲区ART Accelerator和指令/数据缓存ICache/DCache。如果在Reset_Handler里不配置CPU会以极低的效率从Flash取指导致启动时间过长甚至影响实时性。内存保护单元MPU配置在需要内存隔离的安全应用中Reset_Handler需要初始化MPU为不同的内存区域如代码、数据、外设设置访问权限。配置错误会导致后续的内存访问触发MemManage Fault。时钟树的“二次确认”startup.s里配置的时钟只是软件层面的设置。Reset_Handler可以读取RCC相关寄存器如RCC_CFGR, RCC_CR确认HSI/HSE/PLL的状态是否真的如预期那样稳定。如果不稳定可以在此处加入更长的等待循环或者跳转到一个错误处理函数。关键外设的“最小化初始化”例如为了在启动失败时能通过LED给出指示Reset_Handler可以在拷贝数据段之前就配置好一个GPIO引脚并点亮一个LED。这样即使后续的初始化失败你也能知道CPU至少跑到了这里。这些扩展使得Reset_Handler不再是千篇一律的模板而是一个承载了项目特定需求的、活的启动入口。它的代码质量直接决定了系统的鲁棒性和可维护性。3.3 链接脚本Linker Script内存布局的“宪法”一切的源头链接脚本通常为.ld文件是整个启动流程的“宪法”。它定义了程序在内存中的终极布局startup.s和C代码都必须严格遵守它。一个典型的STM32链接脚本片段如下/* 定义内存区域 */ MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 512K RAM (rwx) : ORIGIN 0x20000000, LENGTH 64K } /* 定义程序段 */ SECTIONS { .vectors : { . ALIGN(4); KEEP(*(.vectors)) . ALIGN(4); } FLASH .text : { . ALIGN(4); *(.text) *(.rodata) . ALIGN(4); } FLASH .data : AT (ADDR(.text) SIZEOF(.text)) { . ALIGN(4); _sdata .; *(.data) _edata .; } RAM .bss : { . ALIGN(4); _sbss .; *(.bss) *(COMMON) _ebss .; } RAM /* 栈和堆 */ _estack ORIGIN(RAM) LENGTH(RAM); _Min_Stack_Size 0x400; }这个脚本定义了三个核心要素MEMORY声明了可用的物理内存区域及其属性rx表示可读可执行rwx表示可读可写可执行。SECTIONS定义了各个程序段.vectors,.text,.data,.bss在内存中的位置和大小。符号定义_sdata,_edata,_sbss,_ebss,_estack等符号是startup.s文件中用来定位数据的“路标”。它们的值完全由链接脚本计算得出。最致命的错误往往源于链接脚本的“越界”。例如你定义了一个巨大的全局数组uint8_t big_buffer[100000];它会被分配到.bss段。如果链接脚本中RAM的LENGTH只有64K65536字节而big_buffer加上其他所有.bss变量的总和超过了这个长度链接器并不会报错而是会默默地将.bss段的结束地址_ebss设置为一个超出RAM范围的值。当startup.s执行清零BSS段的循环时就会向非法地址写入数据触发BusFault。另一个常见陷阱是.data段的AT属性。AT (ADDR(.text) SIZEOF(.text))表示.data段在Flash中的存放位置紧随.text段之后。如果.text段太大超出了FLASH的LENGTH链接器会报错。但如果.text段刚好卡在边界上而.data段的AT地址又恰好落在了Flash的末尾那么.data段的初始化值就会被截断导致RAM中拷贝过来的数据不完整。排查技巧编译完成后使用arm-none-eabi-size -A your_project.elf命令可以查看各个段的大小。使用arm-none-eabi-objdump -h your_project.elf可以查看每个段的虚拟地址VMA和加载地址LMA。将这两个地址与你的链接脚本对比就能一眼看出是否有越界风险。4. 实操全过程从零开始手把手构建一个可验证的启动流程4.1 工具链准备与最小工程创建我们以STM32F103C8T6俗称“蓝 pill”为例使用GNU ARM工具链arm-none-eabi-gcc进行实操。整个过程不依赖任何IDE全部使用命令行让你看清每一步的本质。第一步安装工具链在Ubuntu上sudo apt update sudo apt install gcc-arm-none-eabi binutils-arm-none-eabi gdb-arm-none-eabi openocd在Windows上推荐下载GNU Arm Embedded Toolchain的离线安装包。第二步创建项目目录结构stm32_startup/ ├── src/ │ ├── main.c │ └── startup_stm32f103xb.s ├── inc/ │ └── stm32f103xb.h ├── ld/ │ └── stm32f103c8t6.ld ├── build/ └── Makefile第三步编写最简化的startup_stm32f103xb.s我们不使用官方库的庞大startup文件而是手写一个精简版只保留核心功能.syntax unified .cpu cortex-m3 .fpu soft .section .vectors, a .global __Vectors __Vectors: .word _estack /* Top of Stack */ .word Reset_Handler /* Reset Handler */ .word NMI_Handler /* NMI Handler */ .word HardFault_Handler /* Hard Fault Handler */ /* ... 中间省略只保留前8个向量其余填0 */ .rept 256-8 .word 0 .endr .section .text .global Reset_Handler Reset_Handler: /* 初始化MSP */ ldr sp, _estack /* 关中断 */ cpsid i /* 拷贝.data段 */ ldr r0, _sdata ldr r1, _edata ldr r2, _sidata movs r3, #0 b LoopCopyDataInit CopyDataInit: ldrb r4, [r2, r3] strb r4, [r0, r3] adds r3, r3, #1 LoopCopyDataInit: adds r3, r0, r3 cmp r3, r1 ble CopyDataInit /* 清零.bss段 */ ldr r0, _sbss ldr r1, _ebss movs r2, #0 b LoopFillZerobss FillZerobss: strb r2, [r0], #1 cmp r0, r1 ble FillZerobss /* 跳转到main */ bl main b . .weak NMI_Handler .weak HardFault_Handler .weak Default_Handler .thumb_set NMI_Handler, Default_Handler .thumb_set HardFault_Handler, Default_Handler .thumb_set Default_Handler, .第四步编写stm32f103c8t6.ld链接脚本MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 64K RAM (rwx) : ORIGIN 0x20000000, LENGTH 20K } SECTIONS { .vectors : { . ALIGN(4); KEEP(*(.vectors)) . ALIGN(4); } FLASH .text : { . ALIGN(4); *(.text) *(.rodata) . ALIGN(4); } FLASH .data : AT (ADDR(.text) SIZEOF(.text)) { . ALIGN(4); _sdata .; *(.data) _edata .; } RAM .bss : { . ALIGN(4); _sbss .; *(.bss) *(COMMON) _ebss .; } RAM _estack ORIGIN(RAM) LENGTH(RAM); _Min_Stack_Size 0x200; }第五步编写main.c#include stm32f103xb.h // 定义一个全局变量用于验证.data段拷贝 volatile uint32_t global_var 0x12345678; // 定义一个未初始化的全局变量用于验证.bss段清零 volatile uint32_t uninit_var; int main(void) { // 配置PA0为推挽输出用于点亮LED RCC-APB2ENR | RCC_APB2ENR_IOPAEN; // 使能GPIOA时钟 GPIOA-CRL ~GPIO_CRL_MODE0; // 清除PA0模式位 GPIOA-CRL | GPIO_CRL_MODE0_0; // 设置PA0为推挽输出10MHz GPIOA-CRL ~GPIO_CRL_CNF0; // 清除PA0配置位 GPIOA-CRL | GPIO_CRL_CNF0_0; // 设置PA0为推挽输出通用 // 主循环翻转PA0 while(1) { GPIOA-BSRR GPIO_BSRR_BS0; // 置位PA0 for(volatile int i0; i1000000; i); GPIOA-BSRR GPIO_BSRR_BR0; // 复位PA0 for(volatile int i0; i1000000; i); } }4.2 编译、链接与烧录见证“从没电到跑main”的全过程第六步编写Makefile# 工具链路径 CC arm-none-eabi-gcc AS arm-none-eabi-gcc LD arm-none-eabi-gcc OBJCOPY arm-none-eabi-objcopy OBJDUMP arm-none-eabi-objdump SIZE arm-none-eabi-size # 目标文件 TARGET firmware SOURCES $(wildcard src/*.c) $(wildcard src/*.s) OBJECTS $(SOURCES:src/%.cbuild/%.o) $(SOURCES:src/%.sbuild/%.o) DEPS $(OBJECTS:.o.d) # 编译选项 CFLAGS -mcpucortex-m3 -mthumb -g -O0 -Wall -Wextra -stdgnu99 CFLAGS -Iinc -Isrc ASFLAGS -mcpucortex-m3 -mthumb -g LDFLAGS -Tld/stm32f103c8t6.ld -nostartfiles -Wl,-Map$(TARGET).map # 规则 all: $(TARGET).bin build/%.o: src/%.c | build $(CC) $(CFLAGS) -MMD -MP -MF build/$*.d -c $ -o $ build/%.o: src/%.s | build $(AS) $(ASFLAGS) -c $ -o $ build: mkdir -p build $(TARGET).elf:
返回列表