
1. 这不是教科书里的“STM32简介”而是一个干了十年嵌入式的老手拆开芯片外壳后想说的话你搜“STM32 简介”出来的结果大概率是意法半导体推出的32位ARM Cortex-M系列微控制器基于ARMv7-M架构主频从几十MHz到几百MHz不等……停。这些话我当年在实验室抄笔记时也背过但真正第一次把STM32F103C8T6焊上板子、烧进第一行LED闪烁代码、结果串口没反应、JTAG连不上、调试器报错“Target not connected”的那个晚上没人告诉我——所谓“简介”从来不是参数表的复读而是你和这块蓝色小芯片之间建立信任关系的第一步。今天说的STM32不是维基百科词条也不是数据手册翻译稿。它是我带过的二十多个应届生新人踩坑合集是某高校电子设计竞赛团队连续三届用同一块最小系统板打完全部赛题的实操路径是某工业传感器模块量产前被反复推翻又重建的五版PCB底层驱动逻辑。核心关键词就三个寄存器级控制、外设协同调度、资源边界意识。它适合谁适合刚焊完第一个USB转TTL模块、正对着Keil界面发呆的大二学生适合手头有块野火/正点原子开发板、但还不敢动stm32f1xx_hal_conf.h里那堆宏定义的转行工程师也适合已经能写FreeRTOS任务调度、却在低功耗模式下电流始终降不下去的资深开发者——因为真正的“简介”从来不是起点而是你每次遇到问题时回过头来重新理解这块芯片的锚点。别急着抄例程。先搞懂一件事STM32不是一台电脑它没有操作系统帮你兜底没有虚拟内存帮你隔离错误没有看门狗自动重启你的死循环。它是一把没鞘的刀你握得稳它切菜快你手一抖划伤的是自己写的初始化代码。接下来我要带你做的不是罗列型号差异而是还原一个真实项目现场——从选型决策那一刻起所有技术判断背后的“为什么”。2. 内容整体设计与思路拆解为什么“简介”必须从“放弃HAL库”开始讲2.1 所有初学者都绕不开的陷阱把“简介”当成“速成指南”我见过太多人打开STM32CubeMX勾选RCC时钟、USART1、GPIOA生成代码编译下载LED亮了串口吐出“Hello World”然后长舒一口气“STM32我学会了。”错。你学会的只是STM32CubeMX的图形界面操作。真正的STM32藏在你没点开的那几行代码里// 自动生成的system_stm32f1xx.c中这一段 RCC-CFGR (uint32_t)~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLMULL); RCC-CFGR | (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9);这行代码在干什么它不是在“配置PLL”而是在直接改写寄存器的特定位。RCC-CFGR是复位控制寄存器Clock Configuration Register的地址是清零操作|是置位操作。你没手动写过这行但它的执行结果决定了CPU主频是8MHz还是72MHz——差9倍。而这个差值会直接导致你的定时器中断周期不准、ADC采样速率失锁、甚至SPI通信丢帧。这不是理论风险是我在某智能电表项目里实测抓到的波形当PLL倍频系数从PLLMULL6误设为PLLMULL9SPI CLK相位偏移了12ns恰好卡在某款国产Flash芯片的建立时间临界点上批量返工300片PCB。所以本节的设计逻辑很明确“简介”的第一课必须是亲手操作寄存器而不是依赖库函数封装。这不是复古情怀而是建立硬件直觉的必经之路。就像学开车先摸方向盘、离合、油门的物理反馈再学自动挡的P/R/N/D档位逻辑。HAL库是D档标准外设库是手动挡而寄存器操作是你蹲在引擎盖下亲手拧紧每一颗螺丝。2.2 型号命名规则不是密码学而是资源地图的坐标系STM32F103C8T6——这串字符不是乱码它是芯片的“身份证资源清单封装说明书”。拆解如下字符段含义实操意义F103产品线子系列F通用型103基础增强型对比F101基础型、F105互联型。意味着它有CAN、USB Device、高级定时器但没有以太网MAC或FSMC总线。选型时若项目需外挂LCD屏并走8080并口F103就不够用得升到F107或F407。C引脚数量C48引脚LQFP48封装。注意同为F103T6是36引脚TSSOP36B6是64引脚LQFP64。引脚数直接决定你能接多少传感器——比如某温湿度采集模块需要I2CUARTADC3个GPIO控制电源48脚刚好卡满64脚才留出余量。8Flash容量864KB Flash注意不是8KB这是新手最大误区。实际可用空间约58KB启动代码、中断向量表占固定位置。某客户项目因OTA升级包超64KB硬生生把算法从浮点全改成定点精度损失0.3%但省下2KB空间。T6封装温度范围TLQFP6工业级-40℃~85℃。民用级是7-20℃~70℃。某车载OBD设备在东北冬季-35℃无法启动换T6版后解决——温度等级不是虚标是硅片掺杂工艺的物理差异。这个命名规则本质是一张资源约束地图。你不需要背全表但必须养成习惯看到型号立刻脑内映射出“我最多能接几个I2C设备”、“ADC通道够不够采8路热电偶”、“Flash剩多少能塞下AES加密算法”。这才是简介该给你的能力而不是告诉你“它有7通道DMA”。2.3 为什么必须强调“时钟树”因为它决定你写的每一行代码是否准时STM32的时钟系统不是背景板它是整个系统的脉搏。F103的时钟树有5个源头HSI内部8MHz RC、HSE外部晶振、LSI低速内部RC、LSE低速外部晶振、PLL锁相环。但真正影响你代码行为的只有两个关键节点SYSCLK系统时钟CPU、NVIC、大部分外设的基准时钟。F103默认是HSE经PLL倍频后72MHz。PCLK1/PCLK2APB总线时钟PCLK1带I2C/USART2-3/SPI2最大36MHzPCLK2带USART1/SPI1/定时器最大72MHz。问题来了如果你用HAL_UART_Transmit()发送数据波特率计算公式是USARTDIV (usart_ker_ck) / (16 * BaudRate)其中usart_ker_ck取决于你把USART1挂在PCLK2上72MHz还是USART2挂在PCLK1上36MHz。同样设115200波特率前者USARTDIV≈39.06后者≈19.53。如果代码里没配对时钟源实际波特率偏差超5%通信必然失败。我带过的一个学生在调试ESP8266 AT指令时死活收不到响应查了一周硬件最后发现是CubeMX里误把USART1时钟源设成了PCLK136MHz而代码里按72MHz算的分频值。改回PCLK2问题秒解。这就是为什么“简介”必须讲时钟树——它不是知识是排错的起点。3. 核心细节解析与实操要点从点亮LED开始每一步都在建立硬件直觉3.1 最小系统三要素电源、复位、时钟缺一不可很多初学者以为“接上USB供电就能跑”结果发现下载器连不上。真相是STM32的最小系统有三个物理层硬性条件缺一不可。第一电源必须干净且分压合理F103要求VDD/VSS电压在2.0V~3.6V之间典型值3.3V。但关键不是电压值而是纹波。实测数据当输入电源纹波100mVpp时内部RC振荡器HSI频率漂移达±5%导致SysTick中断不准。某医疗设备项目因此出现心率计算误差最终在VDD引脚并联一个10μF钽电容0.1μF陶瓷电容纹波压至20mVpp以下解决。第二复位电路必须满足tRST时间NRST引脚低电平持续时间需≥10μs才能可靠复位。常见错误是直接用10kΩ上拉100nF电容构成RC复位但上电瞬间电容充电曲线缓慢可能仅维持5μs低电平。正确做法是加一片专用复位芯片如TPS3823或至少用1kΩ上拉100nF电容时间常数缩短10倍。第三时钟源必须匹配负载电容HSE外部晶振通常8MHz要求匹配两个20pF负载电容。但很多开发板为节省成本只焊一个电容或用30pF凑数。结果是晶振启振困难尤其低温环境。实测-20℃下20pF匹配时启振时间2ms30pF时延长至15ms超出STM32内部复位超时阈值直接黑屏。提示焊接前务必用万用表二极管档测NRST引脚对地电阻正常应为无穷大未短路。曾有个批次PCB因蚀刻残留导致NRST对地阻值仅200Ω所有板子都无法下载返工三天。3.2 GPIO初始化四步法不是调函数而是改寄存器以PA5输出控制LED为例标准流程是使能GPIOA时钟RCC-APB2ENR | RCC_APB2ENR_IOPAEN; // APB2总线GPIOA挂在此处注意F103有APB1/APB2两条总线GPIOA/B/C/E在APB2GPIOC/D/F/G在APB1。选错总线GPIO根本不会响应。配置GPIO模式GPIOA-CRH ~(0xF 20); // 清除PA5原配置高4位 GPIOA-CRH | (0x2 20); // CNF5[1:0]00推挽输出MODE5[1:0]10最大输出速度2MHz关键点CRH控制高8位PA8~PA15CRL控制低8位PA0~PA7。PA5属于低8位应操作CRL而非CRH这是新人最高频错误。设置初始电平GPIOA-BSRR GPIO_BSRR_BS5; // 置位PA5LED灭因共阳接法验证寄存器状态下载后用ST-Link Utility读取0x40010800GPIOA_BASE地址确认CRL寄存器第20~23位为0010BSRR为0x00200000。这步不能省——它证明你真的控制了硬件而非依赖库的“黑盒”。注意推挽输出模式下若外接LED未串限流电阻GPIO引脚电流可能超25mA绝对最大额定值导致IO口永久损坏。实测直接接3.3V LED10分钟后PA5输出电压跌至2.1V已失效。3.3 中断向量表不是概念而是内存里的一张固定表格当你写NVIC_EnableIRQ(USART1_IRQn)本质是往地址0xE000E100 (IRQn * 4)写1。F103的中断向量表起始地址是0x08000000Flash首地址共60个入口每个4字节存放对应中断服务程序ISR的地址。关键细节向量表可重定向。默认在Flash但若你启用Bootloader需将向量表移到SRAM0x20000000。此时必须执行SCB-VTOR 0x20000000; // 设置向量表偏移寄存器 __DSB(); __ISB(); // 数据/指令同步屏障确保生效否则中断永远跳转到Flash地址而你的ISR在SRAM里结果就是HardFault。某固件升级项目因此卡死排查三天才发现VTOR没设。更隐蔽的坑某些IDE如IAR默认不生成向量表需手动在.icf链接文件中添加place at address mem:0x08000000 { readonly section .intvec };否则编译生成的bin文件开头4字节不是栈顶地址而是随机值芯片根本无法启动。4. 实操过程与核心环节实现从裸机到稳定运行的七步通关4.1 第一步搭建裸机工程拒绝任何库纯汇编寄存器目标不依赖任何库用汇编写启动代码C语言写主循环点亮LED。工程结构如下project/ ├── startup_stm32f103xb.s // 汇编启动文件 ├── system_stm32f103xb.c // 系统时钟初始化纯寄存器 ├── main.c // 主函数GPIO操作 ├── stm32f103xb.ld // 链接脚本指定Flash/SRAM地址 └── Makefile // GNU Arm工具链编译startup_stm32f103xb.s核心段.section .isr_vector .word 0x20005000 /* 栈顶地址 */ .word Reset_Handler /* 复位向量 */ .word NMI_Handler /* NMI向量 */ /* ... 其他58个向量 */ Reset_Handler: ldr r0, _stack_end /* 加载栈顶 */ mov sp, r0 bl SystemInit /* 调用C函数初始化时钟 */ bl main /* 跳转main */ b .system_stm32f103xb.c关键代码void SystemInit(void) { // 1. 清除RCC寄存器复位值 RCC-CR 0x00000001; RCC-CFGR 0x00000000; RCC-CIR 0x00000000; // 2. 使能HSE RCC-CR | RCC_CR_HSEON; while(!(RCC-CR RCC_CR_HSERDY)); // 等待HSE就绪 // 3. 配置PLLHSE*972MHz RCC-CFGR ~RCC_CFGR_PLL; RCC-CFGR | RCC_CFGR_PLLMULL9 | RCC_CFGR_PLLSRC_HSE; RCC-CR | RCC_CR_PLLON; while(!(RCC-CR RCC_CR_PLLRDY)); // 4. 切换SYSCLK到PLL RCC-CFGR ~RCC_CFGR_SW; RCC-CFGR | RCC_CFGR_SW_PLL; while((RCC-CFGR RCC_CFGR_SWS) ! RCC_CFGR_SWS_PLL); // 5. 配置AHB/APB分频 RCC-CFGR | RCC_CFGR_HPRE_DIV1; // AHB 72MHz RCC-CFGR | RCC_CFGR_PPRE1_DIV2; // APB1 36MHz RCC-CFGR | RCC_CFGR_PPRE2_DIV1; // APB2 72MHz }为什么坚持裸机因为只有亲手写过这段你才会明白while(!(RCC-CR RCC_CR_HSERDY))不是“等待”而是轮询硬件状态标志位这是嵌入式最原始的交互方式RCC-CFGR | RCC_CFGR_SW_PLL这行执行后CPU时钟立即切换若后续代码有延时循环必须按新频率重算启动文件里.word 0x20005000的栈顶地址必须严格等于链接脚本中_stack_end符号值否则局部变量一用就溢出。4.2 第二步串口通信——用寄存器实现115200波特率精准收发目标不调HAL_UART_Init()纯寄存器配置USART1实现字符串回显。关键参数计算USARTDIV (usart_ker_ck) / (16 × BaudRate)F103中USART1时钟源为PCLK272MHz故USARTDIV 72000000 / (16 × 115200) 39.0625整数部分390x27小数部分0.0625 → 乘16得10x1故BRR0x271。寄存器配置流程// 1. 使能USART1和GPIOA时钟 RCC-APB2ENR | RCC_APB2ENR_USART1EN | RCC_APB2ENR_IOPAEN; // 2. 配置PA9/PA10为复用推挽 GPIOA-CRL ~(0xFF 4); // PA9/PA10 GPIOA-CRL | (0xB4 4); // CNF910,CNF1010; MODE911,MODE1011 // 3. 配置USART1 USART1-BRR 0x271; // 波特率 USART1-CR1 USART_CR1_UE | USART_CR1_TE | USART_CR1_RE; // 使能、发送、接收 USART1-CR2 0; // 无停止位配置 USART1-CR3 0; // 无硬件流控 // 4. 发送函数轮询 void usart1_send_byte(uint8_t data) { while(!(USART1-SR USART_SR_TC)); // 等待发送完成 USART1-DR data; } // 5. 接收函数轮询 uint8_t usart1_recv_byte(void) { while(!(USART1-SR USART_SR_RXNE)); // 等待接收完成 return USART1-DR; }实操心得while(!(USART1-SR USART_SR_TC))中TCTransmit Complete标志位表示数据已从移位寄存器发出而非仅写入DR寄存器。若用TXETransmit Data Register Empty标志可能在数据还在移位时就写入下一字节导致丢帧。某项目因误用TXE在1Mbps高速通信下丢包率达12%改用TC后降至0.001%。接收时务必先查RXNE再读DR否则读空寄存器会触发OREOverrun Error需手动清标志位USART1-SR ~USART_SR_ORE。4.3 第三步SysTick定时器——构建毫秒级精准延时目标不用HAL_Delay()用SysTick实现1ms滴答支撑后续任务调度。关键点SysTick是Cortex-M内核外设不经过APB总线时钟源为AHB/8F103中AHB72MHz故SysTick_CLK9MHz。配置代码void SysTick_Init(void) { // 1. 设置重装载值9MHz / 1000Hz 9000 SysTick-LOAD 9000 - 1; // 计数器从LOAD递减至0产生中断 // 2. 清空当前值 SysTick-VAL 0; // 3. 配置时钟源外部时钟即AHB/8使能中断和计数器 SysTick-CTRL SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk; } // 全局毫秒计数器 volatile uint32_t ms_ticks 0; void SysTick_Handler(void) { ms_ticks; } // 延时函数 void delay_ms(uint32_t ms) { uint32_t start ms_ticks; while((ms_ticks - start) ms); }为什么不用HAL_DelayHAL_Delay()底层也是SysTick但它在HAL_IncTick()中做了额外判断if (__HAL_SYSTICK_GET_FLAG() ! RESET) { HAL_IncTick(); }而__HAL_SYSTICK_GET_FLAG()本质是读SysTick-CTRL SysTick_CTRL_COUNTFLAG_Msk。但在高优先级中断中若SysTick中断被屏蔽此标志可能丢失导致HAL_IncTick()少执行一次HAL_Delay(1000)实际延时999ms。裸机方案直接读全局变量无中间层损耗精度更高。4.4 第四步ADC采样——单通道连续转换的寄存器实战目标用ADC1采样PA0电压12位精度连续转换模式DMA搬运结果。关键步骤时钟与GPIO配置RCC-APB2ENR | RCC_APB2ENR_ADC1EN; // 使能ADC1时钟 GPIOA-CRL ~(0xF 0); // PA0模式清零 GPIOA-CRL | (0x04 0); // 模拟输入模式ADC初始化ADC1-CR2 0; // 先清零 ADC1-CR1 ADC_CR1_SCAN; // 扫描模式虽单通道也需开启 ADC1-CR2 ADC_CR2_ADON | // 开启ADC ADC_CR2_CONT | // 连续转换 ADC_CR2_EXTSEL | // 外部触发此处禁用 ADC_CR2_SWSTART; // 软件启动 ADC1-SMPR2 0x00000007; // PA0采样时间239.5周期最大 ADC1-SQR3 0x00000000; // 规则序列第1个通道0PA0DMA配置搬运ADC_DR到内存RCC-AHBENR | RCC_AHBENR_DMA1EN; DMA1_Channel1-CPAR (uint32_t)ADC1-DR; // 外设地址 DMA1_Channel1-CMAR (uint32_t)adc_buffer; // 内存地址 DMA1_Channel1-CNDTR BUFFER_SIZE; // 传输数量 DMA1_Channel1-CCR DMA_CCR_EN | DMA_CCR_DIR | DMA_CCR_MINC | DMA_CCR_CIRC; ADC1-CR2 | ADC_CR2_DMA; // 使能ADC-DMA请求避坑经验ADC1-SMPR2 0x00000007中的7对应采样周期239.5个ADCCLK周期。若信号源阻抗10kΩ需加大采样时间否则采样值偏低。实测接100kΩ电位器时采样值比真实电压低0.2V改用0x0000000F718.5周期后准确。DMA缓冲区adc_buffer必须定义为__attribute__((aligned(4))) uint16_t adc_buffer[BUFFER_SIZE]否则DMA访问未对齐地址会触发HardFault。某项目因此反复重启查了两天才发现是数组未对齐。4.5 第五步FreeRTOS移植——在裸机基础上嫁接实时内核目标将FreeRTOS移植到裸机工程创建两个任务LED闪烁500ms、串口打印1000ms。关键动作修改启动文件将Reset_Handler末尾的bl main改为bl FreeRTOS_main并在FreeRTOS_main中调用vTaskStartScheduler()。配置FreeRTOSConfig.h#define configUSE_PREEMPTION 1 #define configUSE_TIMERS 0 // 暂不启用软件定时器 #define configUSE_MUTEXES 0 // 暂不启用互斥量 #define configCPU_CLOCK_HZ (72000000UL) #define configSYSTICK_CLOCK_HZ (configCPU_CLOCK_HZ / 8) // SysTick时钟9MHz #define configTICK_RATE_HZ (1000) // 1ms滴答 #define configTOTAL_HEAP_SIZE ((size_t)(32 * 1024)) // 32KB堆重写SysTick_Handlervoid SysTick_Handler(void) { // 不再更新ms_ticks改由FreeRTOS接管 xPortSysTickHandler(); }创建任务void vLEDTask(void *pvParameters) { for(;;) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); vTaskDelay(500 / portTICK_PERIOD_MS); // 500ms } } void vUartTask(void *pvParameters) { for(;;) { printf(RTOS running...\r\n); vTaskDelay(1000 / portTICK_PERIOD_MS); } } int main(void) { HAL_Init(); // 此处HAL_Init仅初始化SysTick不启用其他外设 SystemClock_Config(); // 同上 MX_GPIO_Init(); xTaskCreate(vLEDTask, LED, 128, NULL, 1, NULL); xTaskCreate(vUartTask, UART, 128, NULL, 1, NULL); vTaskStartScheduler(); for(;;); }移植难点解析configSYSTICK_CLOCK_HZ必须设为configCPU_CLOCK_HZ / 8因为FreeRTOS的xPortSysTickHandler()默认使用AHB/8作为SysTick时钟源。若设错vTaskDelay()时间严重失准。vTaskStartScheduler()会关闭所有中断__disable_irq()然后启动PendSV和SysTick中断。若你在main()中提前开启了其他中断如EXTI必须在vTaskStartScheduler()前关闭否则内核无法接管中断。某项目因未关闭EXTI中断导致按键中断抢占RTOS调度任务切换混乱现象是LED闪烁频率忽快忽慢。4.6 第六步低功耗模式——STOP模式下电流从20mA降至15μA目标让MCU进入STOP模式仅RTC和待机电路工作电流≤20μA。关键步骤配置RTC时钟源RCC-BDCR | RCC_BDCR_LSEON; // 使能LSE32.768kHz while(!(RCC-BDCR RCC_BDCR_LSERDY)); RCC-BDCR | RCC_BDCR_RTCSEL_LSE; // RTC时钟源LSE RCC-BDCR | RCC_BDCR_RTCEN; // 使能RTC进入STOP模式void enter_stop_mode(void) { PWR-CR | PWR_CR_LPDS; // 低功耗深度睡眠 SCB-SCR | SCB_SCR_SLEEPDEEP_Msk; // 深度睡眠位 __WFI(); // 等待中断WFI指令 }唤醒配置以EXTI0为例EXTI-IMR | EXTI_IMR_MR0; // 使能EXTI0中断 EXTI-FTSR | EXTI_FTSR_TR0; // 下降沿触发 NVIC_EnableIRQ(EXTI0_IRQn);实测数据默认运行模式20.3mA72MHz所有外设开启STOP模式LSERTCEXTI018.7μA待机模式Standby2.1μA但会丢失SRAM内容需从备份域恢复致命陷阱进入STOP前必须关闭所有APB/AHB外设时钟否则其漏电流会拖垮整体功耗。实测若忘记关闭ADC时钟STOP电流飙升至800μA。GPIO必须配置为模拟输入或上拉/下拉输入禁止悬空。悬空引脚会因噪声反复翻转导致IO口漏电。某电池供电项目因此续航从6个月降至3周最终将所有未用IO设为GPIO_MODE_ANALOG解决。4.7 第七步固件升级IAP——用串口实现APP区无缝更新目标通过串口接收新固件bin文件擦写Flash APP区0x08003000起不依赖ST-Link。核心逻辑Bootloader与APP分区Bootloader0x08000000 ~ 0x08002FFF12KBAPP0x08003000 ~ 0x0801FFFF120KB跳转到APP的关键代码typedef void (*pFunction)(void); pFunction Jump_To_Application; uint32_t JumpAddress; // 检查APP区首地址是否有效栈顶地址需在SRAM范围内 if (((*(__IO uint32_t*)APP_ADDR) 0x2FFE0000) 0x20000000) { JumpAddress *(__IO uint32_t*)(APP_ADDR 4); // 获取复位向量地址 Jump_To_Application (pFunction)JumpAddress; __set_MSP(*(__IO uint32_t*)APP_ADDR); // 设置主栈指针 Jump_To_Application(); // 跳转 }Flash擦写流程FLASH_Unlock(); // 解锁Flash FLASH_ErasePage(APP_PAGE_START); // 擦除页1KB for(uint16_t i0; i1024; i) { FLASH_ProgramHalfWord(APP_ADDR i*2, data[i]); // 半字编程 } FLASH_Lock(); // 上锁血泪教训__set_MSP()必须在跳转前执行否则APP启动时栈指针指向非法地址立即HardFault。Flash编程前必须擦除且只能按页擦除F103为1KB页。若新固件小于一页仍需擦除整页否则旧数据残留导致校验失败。某项目因未校验擦除后状态FLASH_GetFlagStatus(FLASH_FLAG_BSY)在擦除未完成时就开始编程导致Flash锁死整片报废。5. 常见问题与排查技巧实录那些手册里不会写的“人话答案”5.1 串口下载失败的七种可能以及如何3分钟定位现象最可能原因快速验证法解决方案ST-Link识别到设备但下载失败SWDIO/SWCLK引脚被其他电路拉低用万用表测SWDIO对地电压正常应为3.3V断开所有外接电路仅留最小系统下载器显示“Target not connected”NRST引脚未释放被外部电路拉低测NRST对地电压正常应为3.3V检查复位