STM32F103标准外设库3.6.0最小工程搭建与避坑指南
发布时间:2026/10/26 12:15:46 锦皓数字建站

简介STM32F10x标准库3.6.0最终版面向使用STM32F10x系列MCU的嵌入式初学者与开发工程师核心价值在于把底层寄存器操作封装为可调用的外设库函数减少直接配置寄存器的负担提升代码可读性与工程复用性。压缩包共960个文件、约21.25MB以349个C源文件和277个头文件为主体辅以txt说明文档、汇编启动文件、s/lsl/ld/icf等链接脚本以及Keil、IAR等工程配置较完整地保留了标准外设库的源码、示例与移植所需材料。资源内附操作说明书便于按外设模块查阅库函数用法、工程组织方式和启动流程。已有2823人学习下载适合用来搭建F10x开发模板、对照学习GPIO/时钟/中断等外设驱动写法或作为旧项目维护与代码迁移的参考底包。1. 一块 F103 到手先决定底层用哪套一块 F103C8 的最小系统板摆在桌上第一件事往往不是敲main而是先定底层。寄存器直写灵活换个人接手就看不懂HAL 抽象度高一层套一层真要抠时序和中断响应还得翻回去重读手册。STM32F10x 标准外设库 3.6.0 是这条路线上的最终版本之后就不再更新了也正因为它不再变动很多人把它当作一份可以长期复用的底子。我更愿意把它看成挂在寄存器上的一套软件插件stm32f10x_conf.h是插件的开关面板需要哪个外设模块就编哪个用不到的关掉编译体积和调用层级都看得见、控得住。正在做课程设计、小批量工控板或者要接手一堆老项目的人从这一版入手最省事。2. 拆开 3.6.0 的目录真正进编译器的只有十来个文件一个完整的库压缩包解出来通常几百个文件其中绝大多数是示例工程和评估板驱动。第一次接触的人容易把整个目录拖进 IDE结果编译几百个 .c报了上百个错还没开始写业务代码就先失败。实际做法是先分清哪些文件参与编译、哪些只当参考。2.1 四层结构与编译边界库的组织方式很规整Libraries放内核和外设驱动Project放模板工程与示例Utilities放评估板专用代码。只有.c和.s会进编译器.h只是被引用。把边界理清楚编译文件数能从几百个压到十几个。路径作用是否参与编译Libraries/CMSIS/CM3/CoreSupport/core_cm3.c内核寄存器定义、NVIC/SysTick 内联函数GCC 下通常不编头文件里已有静态内联版本Libraries/CMSIS/CM3/DeviceSupport/.../system_stm32f10x.c复位后时钟初始化SystemInit()必须编且全工程只能一份.../startup/arm/startup_stm32f10x_md.s向量表、复位入口、默认中断处理必须编按 Flash 容量选型号只能一份Libraries/STM32F10x_StdPeriph_Driver/src/*.c各外设驱动按需最小集合是 gpio/rcc/usart/miscProject/STM32F10x_StdPeriph_Template/*模板main.c、stm32f10x_it.c、stm32f10x_conf.h拷出来用模板工程本身不编Utilities/STM32_EVAL/*评估板按键、LCD、SPI Flash 驱动不编除非手上正好是那块板core_cm3.c这个文件要单独说明。它里面用了一堆编译器相关的宏在较新的 GCC 版本下经常报__ASM未定义或者__STDC_VERSION__冲突。头文件core_cm3.h里已经把 NVIC 那批函数写成了__STATIC_INLINE所以 GCC 工具链下直接不编这个 .c只留头文件就行功能一点都不缺。用 MDK 的 ARMCC 编译时编不编都行但一旦发现NVIC_SetPriority重复定义就是两边都编了。2.2 三个宏决定编进来什么这套库的所有裁剪都靠预处理宏写错一个症状往往很隐蔽不是编译报错而是运行时行为不对。# GCC 下把全局宏写进 Makefile比藏在 IDE 选项里更不容易在换机器时丢失 CFLAGS -DUSE_STDPERIPH_DRIVER \ -DSTM32F10X_MD \ -DHSE_VALUE8000000USE_STDPERIPH_DRIVER的作用是让stm32f10x.h去包含stm32f10x_conf.h。少了它RCC_APB2PeriphClockCmd这类函数会在链接阶段报未定义引用因为对应的声明压根没被引入。STM32F10X_MD决定芯片密度等级。中容量是 64K 或 128K Flash大容量STM32F10X_HD小容量STM32F10X_LD互联型STM32F10X_CL。这个宏影响中断向量表长度和外设基址定义选错最典型的后果是中断服务函数名对不上、进了默认死循环。HSE_VALUE是外部晶振频率单位 Hz。它不改变硬件只改变库里的计算结果SystemInit()拿它算 PLL 倍频RCC_GetClocksFreq()拿它反推各条总线频率USART_Init()又拿总线频率算波特率分频。三个地方共用同一个值写错一次全盘偏。2.3 最小可编译集合从几百个文件砍到十几个从零手工挑文件容易漏我一般写个脚本一次性把骨架铺好后面所有工程都从这个骨架复制。#!/usr/bin/env bash # 从解压出的库根目录装配最小工程骨架 set -e LIB./STM32F10x_StdPeriph_Lib_V3.6.0 OUT./project mkdir -p $OUT/{src,inc,startup,ld} # 1. 内核与设备层 cp $LIB/Libraries/CMSIS/CM3/DeviceSupport/*/system_stm32f10x.c $OUT/src/ cp $LIB/Libraries/CMSIS/CM3/DeviceSupport/*/system_stm32f10x.h $OUT/inc/ cp $LIB/Libraries/CMSIS/CM3/DeviceSupport/*/stm32f10x.h $OUT/inc/ cp $LIB/Libraries/CMSIS/CM3/CoreSupport/core_cm3.h $OUT/inc/ # 2. 启动文件中容量 cp $LIB/Libraries/CMSIS/CM3/DeviceSupport/*/startup/arm/startup_stm32f10x_md.s $OUT/startup/ # 3. 最小外设驱动集合 for f in stm32f10x_rcc stm32f10x_gpio stm32f10x_usart misc; do cp $LIB/Libraries/STM32F10x_StdPeriph_Driver/inc/$f.h $OUT/inc/ cp $LIB/Libraries/STM32F10x_StdPeriph_Driver/src/$f.c $OUT/src/ done # 4. 模板里的中断入口与配置头 cp $LIB/Project/STM32F10x_StdPeriph_Template/stm32f10x_it.c $OUT/src/ cp $LIB/Project/STM32F10x_StdPeriph_Template/stm32f10x_it.h $OUT/inc/ cp $LIB/Project/STM32F10x_StdPeriph_Template/stm32f10x_conf.h $OUT/inc/这段脚本做的事就是把“哪些文件必须进工程”固化下来。stm32f10x_conf.h是裁剪面板里面每个外设一行#include stm32f10x_xxx.h注释掉就相当于把这个外设模块从编译里摘出去源文件留着也不影响。stm32f10x_it.c是中断服务函数的落脚点模板里只写了 HardFault 和 SysTick其他自己往里加不要散落在各个业务文件里。提示stm32f10x_conf.h里还有一行#define assert_param(expr)默认打开时会展开成断言检查关掉可以省几百字节 Flash。新手阶段建议先留着参数填错能当场停住。3. 从零建工程MDK 与 GCC 两条路的六个开关同一个库两种工具链的配置项完全不同但对不上就会出同样的问题编译过了但跑飞或者能跑但跑在半速。3.1 MDK 里真正要动的只有四处新建工程、选好器件型号之后剩下需要人工确认的就那么几项。第一处是 C/C 的 Define 框填USE_STDPERIPH_DRIVER, STM32F10X_MD, HSE_VALUE8000000。第二处是 Include Paths至少要有内核目录、设备目录、外设 inc 目录三行。第三处是启动文件在工程树里只能出现一份startup_stm32f10x_md.s如果同时加了startup_stm32f10x_hd.s链接时会报Reset_Handler重复定义。第四处是 Target 里的Use MicroLIB只有做了printf重定向才需要勾勾了之后库会走一套精简的 C 运行时体积小但不支持某些格式化特性比如浮点不带%f就别指望了。Flash Download 算法要跟芯片容量匹配。F103C8 是 64K选 128K 的算法能下载成功但链接脚本按 64K 算超了就会在运行时莫名进 HardFault。这个坑在高仿片子上尤其常见——标称 64K 实际 128K或者反过来。稳妥做法是以链接脚本为准别依赖下载器探测。3.2 GCC链接脚本、启动文件与-mcpu参数交叉编译环境用arm-none-eabi-那一套Makefile 里的关键几行决定了工程能不能跑起来。TARGET f103_demo CROSS arm-none-eabi- CC $(CROSS)gcc OBJCOPY $(CROSS)objcopy CFLAGS -mcpucortex-m3 -mthumb -O2 -g3 -Wall \ -ffunction-sections -fdata-sections \ -DUSE_STDPERIPH_DRIVER -DSTM32F10X_MD -DHSE_VALUE8000000 \ -Iinc LDFLAGS -Tld/stm32f103c8.ld -nostartfiles \ -Wl,--gc-sections -Wl,-Map$(TARGET).map \ -Wl,--print-memory-usage SRCS : $(wildcard src/*.c) startup/startup_stm32f10x_md.s OBJS : $(SRCS:.c.o) $(TARGET).elf: $(OBJS) $(CC) $(CFLAGS) $^ $(LDFLAGS) -o $ $(TARGET).bin: $(TARGET).elf $(OBJCOPY) -O binary $ $-mcpucortex-m3 -mthumb这两项省不掉少了会按 ARM 指令集生成链接阶段就报找不到 thumb 的库。-ffunction-sections -fdata-sections配合--gc-sections能把没用到的库函数整段剔除在 64K Flash 上这不是优化而是必需——不开的话光标准库和外设驱动就能吃掉一多半空间。--print-memory-usage每次链接都会打一行 RAM 和 Flash 的占用百分比比事后翻 map 文件快得多。链接脚本里两块内存要对上板子实际情况/* stm32f103c8.ld 片段从 Flash 起始地址放代码RAM 从 0x20000000 开始 */ MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 64K RAM (rwx) : ORIGIN 0x20000000, LENGTH 20K } _estack ORIGIN(RAM) LENGTH(RAM); /* 栈顶放在 RAM 末尾向下生长 */-nostartfiles是配合自己的启动文件用的避免链接器再塞一份标准启动代码进来症状和 MDK 里双份启动文件一模一样。3.3system_stm32f10x.c72MHz 是怎么凑出来的SystemInit()在启动文件里被复位向量调用比main()早得多。它最终把时钟推到多少取决于文件顶部这一行的取舍。/* system_stm32f10x.c 顶部同一时刻只能打开一个频率档 */ #define SYSCLK_FREQ_72MHz 72000000里面SetSysClockTo72()的执行顺序是开 HSE 并等就绪、开 Flash 预取、设 2 个等待周期、配置 PLL 源为 HSE 不分频、倍频系数 9、开 PLL 等锁定、切换 SYSCLK 到 PLL、然后设 AHB 不分频、APB2 不分频、APB1 二分频。算下来 HSE 8MHz × 9 72MHz 给 SYSCLKAHB 72MHzAPB2 72MHzAPB1 36MHz。APB1 上限就是 36MHz超了 USART2/3、I2C、SPI2 这些挂在低速总线上的外设会随机抽风属于典型的“代码没错但就是不稳”。Flash 等待周期这一步最容易被自己改时钟时漏掉。72MHz 下必须要 2 个 wait state库里FLASH_SetLatency(FLASH_Latency_2)已经写了。如果有人照着手册自己写寄存器版本把这条忘了程序能跑但在中断密集时随机取指错误症状是偶尔跳进 HardFault非常难查。4. 四个外设的落地写法参数怎么算、边界在哪库函数本身不长难的是每个参数背后的约束。下面四个模块覆盖了绝大多数入门项目要用的东西。4.1 GPIO 与 USART时钟顺序和波特率误差static void uart1_init(uint32_t baud) { GPIO_InitTypeDef gpio; USART_InitTypeDef uart; /* 先开时钟再碰寄存器。顺序反了不报错只是配置没生效 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); gpio.GPIO_Pin GPIO_Pin_9; /* TX复用推挽 */ gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); gpio.GPIO_Pin GPIO_Pin_10; /* RX浮空输入外部上拉更稳 */ gpio.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, gpio); uart.USART_BaudRate baud; uart.USART_WordLength USART_WordLength_8b; uart.USART_StopBits USART_StopBits_1; uart.USART_Parity USART_Parity_No; uart.USART_HardwareFlowControl USART_HardwareFlowControl_None; uart.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, uart); USART_Cmd(USART1, ENABLE); }RCC_APB2PeriphClockCmd必须在GPIO_Init之前。GPIOA 的时钟没开时写CRL/CRH寄存器是写不进去的而且不会报任何错表现出来就是“引脚一直是输入状态”。USART_Init内部会调用RCC_GetClocksFreq()拿 PCLK2 的实际频率再算USARTDIV PCLK2 / (16 × baud)。72MHz 下 115200 得到 39.0625库会拆成尾数和分频写进BRR实际误差约 -0.16%在容忍范围内。如果HSE_VALUE写的是 8MHz 而板上焊的是 12MHz 晶振算出来的 PCLK2 就是错的波特率整体偏移串口助手上全是乱码这时候换波特率是治不好的。重定向printf在 MDK 和 GCC 下写法不同/* MDK MicroLIB实现 fputc 即可 */ int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET) { } USART_SendData(USART1, (uint8_t)ch); return ch; }GCC 下要换成_write签名是int _write(int fd, char *buf, int len)并且链接时要带-specsnano.specs。两者的共同点是循环等TXE等TC也可以但会慢一截因为TC要等整个帧移完。4.2 TIMPWM 频率和占空比的算术/* 定时器时钟 72MHzPSC71 得 1MHz 计数ARR999 得 1kHz PWM */ void pwm_init(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tb; TIM_OCInitTypeDef oc; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_TIM1, ENABLE); gpio.GPIO_Pin GPIO_Pin_8; /* TIM1_CH1 */ gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); tb.TIM_Prescaler 72 - 1; tb.TIM_Period 1000 - 1; tb.TIM_CounterMode TIM_CounterMode_Up; tb.TIM_ClockDivision TIM_CKD_DIV1; tb.TIM_RepetitionCounter 0; TIM_TimeBaseInit(TIM1, tb); oc.TIM_OCMode TIM_OCMode_PWM1; oc.TIM_OutputState TIM_OutputState_Enable; oc.TIM_Pulse 300; /* 300/1000 30% 占空比 */ oc.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM1, oc); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); /* TIM1/TIM8 是高级定时器不开主输出引脚永远是低电平 */ TIM_CtrlPWMOutputs(TIM1, ENABLE); }频率公式是72MHz / ((PSC1) × (ARR1))改任何一个都会同时影响频率和分辨率。想要 20kHz 又想要 10 位分辨率先算ARR1 1024再反推PSC1 72000000 / (20000 × 1024) ≈ 3.5取 3 或 4 都会偏这时候只能降分辨率或者换更高主频。这种取舍在电机驱动和 LED 调光里是常态不是配置错误。TIM_CtrlPWMOutputs只有 TIM1 和 TIM8 需要通用定时器 TIM2~TIM5 没有这一步。忘了它的表现是“代码全对示波器上一条直线”我第一次遇到时查了半天时钟树。4.3 ADC DMA为什么采样值只跳一次#define ADC_N 64 static volatile uint16_t adc_buf[ADC_N]; void adc_dma_init(void) { GPIO_InitTypeDef gpio; ADC_InitTypeDef adc; DMA_InitTypeDef dma; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); /* 72/6 12MHzADC 上限 14MHz */ gpio.GPIO_Pin GPIO_Pin_0; gpio.GPIO_Mode GPIO_Mode_AIN; /* 模拟输入不要配成浮空 */ GPIO_Init(GPIOA, gpio); DMA_DeInit(DMA1_Channel1); /* ADC1 固定绑 DMA1 通道 1 */ dma.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; dma.DMA_MemoryBaseAddr (uint32_t)adc_buf; dma.DMA_DIR DMA_DIR_PeripheralSRC; dma.DMA_BufferSize ADC_N; dma.DMA_PeripheralInc DMA_PeripheralInc_Disable; dma.DMA_MemoryInc DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; dma.DMA_Mode DMA_Mode_Circular; dma.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel1, dma); DMA_Cmd(DMA1_Channel1, ENABLE); /* 必须先于 ADC 启动 */ adc.ADC_Mode ADC_Mode_Independent; adc.ADC_ScanConvMode DISABLE; adc.ADC_ContinuousConvMode ENABLE; adc.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; adc.ADC_DataAlign ADC_DataAlign_Right; adc.ADC_NbrOfChannel 1; ADC_Init(ADC1, adc); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); /* 上电后必须校准不校准零点会偏几十个 LSB */ ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1) SET) { } ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1) SET) { } ADC_SoftwareStartConvCmd(ADC1, ENABLE); }几个参数决定采样质量。RCC_PCLK2_Div6把 ADC 时钟压到 12MHz超过 14MHz 转换结果会明显失真。ADC_SampleTime_55Cycles5是采样保持时间信号源内阻越大这个值要越大测高阻信号时用 1.5 周期会得到一堆偏低的读数。校准必须在ADC_Cmd之后、开始转换之前做。最典型的翻车是“缓冲区里只有一个值在变或者全是零”。原因通常是 DMA 启动顺序错了或者DMA_BufferSize写了 64 但DMA_MemoryInc没开64 个数据全砸在数组第一个元素上。5. 避坑与排查五个高频翻车点这些东西在手册上不一定写但在实际板子上会一个一个撞上。5.1 编译通过一上电就 HardFault现象下载成功复位后不进main调试器停在 HardFault 处理函数里。原因启动文件与芯片密度不匹配或者链接脚本的 RAM 长度超过了实际容量。前者导致向量表长度对不上中断号错位后者导致栈顶落到了不存在的地址上第一次函数调用压栈就异常。解决先确认STM32F10X_MD与startup_stm32f10x_md.s一致再打开 map 文件看_estack的值是不是0x2000500020K RAM 的情况。如果用的是标称 64K Flash 的板子把链接脚本改成 64K/20K 再试。5.2 串口乱码换波特率也没用现象串口助手上收到一堆乱码把波特率从 115200 试到 9600 都一样。原因HSE_VALUE与实际晶振不符。典型是小板子上焊的是 8MHz工程里宏定义的却是 12MHzRCC_GetClocksFreq()算出的 PCLK2 是错的USART_Init按错误的基准算分频实际波特率偏离了几十个百分点。解决看板子上的晶振丝印或者干脆用内部 HSI 跑 8MHz 先验证通信通不通。确认之后统一改HSE_VALUE同时检查有没有第二处在system_stm32f10x.c里硬编码了频率。5.3 中断只进一次就再也不来了现象外部中断触发一次后正常之后无论怎么按键都没反应。原因中断服务函数里没清中断标志或者清错了标志位。EXTI 的挂起位在EXTI_PR要在处理完之后写 1 清除如果只在 NVIC 里清 pending 而不动 EXTI中断线会一直保持有效。解决在EXTIx_IRQHandler里先判断EXTI_GetITStatus处理完调EXTI_ClearITPendingBit。串口接收同理USART_GetITStatus(USART1, USART_IT_RXNE)读到数据后硬件自动清但如果开了溢出中断ORE必须手动读一次DR再清。5.4 硬件 I2C 卡在 while 里出不来现象程序停在I2C_WaitOnFlagUntilTimeout或者自己写的while (I2C_GetFlagStatus(...) SET)里。原因I2C 总线被从机拉住了。上电时序不对、从机没准备好、或者上一次通信中途复位导致 SDA 一直低主机永远等不到总线空闲。解决初始化时先手动发 9 个时钟脉冲把从机“抖”醒再走I2C_GenerateSTART。工程上更省事的做法是超时机制——所有等待循环都加计数上限超过就I2C_GenerateSTOP然后重新初始化。某些批次的硬件 I2C 在从机时钟拉伸时确实容易挂死很多人干脆改用 GPIO 模拟两个引脚而已时序自己控。5.5-O2下寄存器读写出错现象Debug 编译一切正常切到-O2之后 ADC 或者状态机行为异常。原因外设寄存器指针没有加volatile。库里的定义是加了的但自己封装的读写函数经常漏。优化器看到循环里反复读同一个地址会把它提到循环外只读一次。解决所有指向外设寄存器的指针都加volatile包括自己写的#define REG (*(volatile uint32_t *)0x4001080C)这种。另外stm32f10x_conf.h里的assert_param在-O2下如果被完全裁掉参数字面量就不会被检查参数填错不会有任何提示这也是切优化等级后行为变化的一个来源。6. 进阶验证让配置自己证明它生效了库函数写得再对最终还是要拿证据说话。我习惯在两处加验证一次是在时钟初始化之后一次是在每个外设配置完成之后。第一处是把 SYSCLK 从 MCO 引脚引出来用频率计或者示波器量一下。/* 把 SYSCLK 引到 PA8量到 72MHz 说明 PLL 真的锁上了而不是配了没生效 */ void mco_init(void) { GPIO_InitTypeDef gpio; RCC_ClocksTypeDef clk; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); gpio.GPIO_Pin GPIO_Pin_8; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); RCC_MCOConfig(RCC_MCO_SYSCLK); /* 应量到 72MHz */ RCC_GetClocksFreq(clk); printf(SYSCLK%lu HCLK%lu PCLK1%lu PCLK2%lu\r\n, clk.SYSCLK_Frequency, clk.HCLK_Frequency, clk.PCLK1_Frequency, clk.PCLK2_Frequency); }RCC_GetClocksFreq打出来的四个数要和理论值完全对上72M、72M、36M、72M。只要有一个不对说明某级分频系数或者 PLL 源选错了此时往下写外设全是白搭。注意 MCO 会占用 PA8如果同一块板子还要用 TIM1_CH1 出 PWM两个功能会打架验证完就把这段去掉。第二处是看 map 文件里的内存分布。--print-memory-usage每次链接都会打一行Flash 用到 80% 以上就要警惕了剩下的空间可能不够放新的外设驱动。RAM 更关键_estack到_ebss之间剩下的才是栈能用的空间递归深一点或者printf格式化长字符串就可能把栈打穿表现是随机复位。第三个习惯是新建工程时先只做三件事点一个灯、从 MCO 出时钟、串口打一行时钟表。三样都对再开始写业务逻辑。这看起来慢但比后面在 HardFault 里翻寄存器快太多了。从那以后我每次换新板子或者换工具链都强制走一遍这套最小验证一次没落过也正因为这样才敢在没示波器的情况下远程给别人的板子改时钟配置。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。