- STM32是什么?它基于什么架构?
- ARM Cortex-M0,M3,M4,M7内核的主要区别是什么?
- 什么是CMSIS?它的作用是什么?
- 描述一下STM32从上电到开始执行main函数的过程
- STM32的供电引脚(VDD,VDDA,VBAT等)分别有什么作用?
- 什么是时钟树?为什么它在STM32中很重要?
- 列举STM32的主要时钟源(HSI, HSE,LSI,LSE,PLL)。
- 如何配置PLL以得到最高的系统时钟?
- 什么是GPIO?STM32的GPIO有几种工作模式?
- 推挽输出与开漏输出有什么区别?
- 如何将一个GPIO引脚配置为上拉输入模式?
- 什么是复用功能?如何将一个PA9引脚配置为USART1_TX?
- 什么是NVIC?它在中断系统中起什么作用?
- STM32的中断优先级是如何分组和管理的?
- 什么是EXTI?它如何工作?
- SysTick定时器有什么用途?
- 看门狗定时器是什么?独立看门狗和窗口看门狗有何区别?
- 如何从待机模式中唤醒STM32?
- 什么是位带操作?它有什么优势?
- 什么是ISP和IAP?它们有何不同?
- 通用定时器有哪些主要功能?
- 如何配置定时器产生一个1kHz的PWM信号?
- PWM输出的频率和占空比由哪些寄存器决定?
- 如何用定时器捕获一个外部脉冲的高电平宽度?
- 什么是定时器的编码器接口模式?如何使用?
- 高级定时器(如TIM1)比通用定时器多了哪些功能?
- ADC的分辨率是什么意思?STM32的ADC通常是多少位?
- 什么是ADC的规则通道和注入通道?
- 如何实现多通道ADC扫描转换?
- ADC的采样时间如何影响转换结果?
- 如何校准ADC?
- 如何用定时器触发一个ADC转换?
- DAC的主要用途是什么?
- 比较器的工作原理是什么?
- 什么是实时时钟?如何配置RTC并产生一个闹钟中断?
- 如何将数据备份到RTC的备份寄存器中?
- 芯片唯一ID有什么用途?
- Flash存储器是如何组织的?什么是页,什么是扇区?
- 如何对内部Flash进行读写操作?
- 什么是选项字节?如何配置它?
- UART通信的基本原理是什么?
- 如何配置UART以实现115200的波特率?
- UART如何通过中断方式接收不定长数据?
- 什么是DMA?它有什么好处?
- 如何配置UART使用DMA进行数据发送和接收?
- SPI有几种工作模式?由什么信号决定?
- 如何配置SPI为主机全双工模式?
- I2C通信的起始信号和停止信号是如何定义的?
- I2C从机地址是如何组成的?
- 如何用软件模拟I2C时序?
- STM32的硬件I2C在应用时需要注意什么?
- 比较UART、SPI和I2C三种通信协议的特点和适用场景。
- CAN总线的基本帧、扩展帧和远程帧有什么区别?
- 如何配置CAN总线过滤器?
- 如何计算CAN总线的波特率?
- SDIO接口和SPI模式驱动SD卡有何区别?
- 如何通过FSMC接口驱动TFT液晶屏?
- 什么是LTDC?它有什么作用?
- DCMI接口用于什么场景?
- 如何用STM32驱动一个WS2812B LED?
- 如何读取旋转编码器的值?
- 如何通过STM32驱动一个步进电机?
- 如何实现一个简单的PID控制器?
- 在STM32上如何运行FreeRTOS?
- 在FreeRTOS中,任务有哪几种状态?
- 什么是信号量、互斥量和消息队列?
- 如何避免任务间的优先级反转?
- 什么是内存堆碎片?如何应对?
- 如何使用STM32CubeMX生成初始化代码?
- STM32CubeMX中生成的HAL库和LL库有什么区别?
- HAL库中的轮询、中断和DMA三种模式有何不同?
- 如何处理HAL库中的超时错误?
- 如何自定义HAL库的回调函数?
- 如何配置STM32进入低功耗模式(睡眠、停止、待机)?
- 在低功耗模式下,哪些外设可以继续工作?
- 如何分析和优化STM32的功耗?
- 如何通过SWD接口调试STM32?
- 断点有哪几种类型?
- 如何通过串口打印调试信息?
- 如何使用ITM进行更高效的调试?
- 如何测量代码的执行时间?
- 在电路设计中,STM32的复位电路和晶振电路需要注意什么?
- 什么是去耦电容?为什么要尽可能靠近芯片电源引脚放置?
- PCB布局布线时,模拟部分和数字部分应如何处理?
- 程序跑飞了,可能的原因有哪些?
- 如何利用看门狗来增强系统的可靠性?
- 在项目开发中,如何进行STM32的固件升级?
- 如何配置STM32实现一个简单的引导程序?
- 什么是内存管理单元?哪些Cortex-M内核拥有它?
- 如何配置MPU以保护不同的内存区域?
- 什么是DSP指令集?哪些STM32系列支持它?
- 如何用STM32实现一个简单的FFT?
- 浮点单元是什么?哪些STM32系列拥有硬件FPU?
- 使用FPU需要注意什么?
- 以太网MAC和PHY的区别是什么?
- 如何配置LWIP协议栈以实现TCP通信?
- USB有哪几种传输类型?各自适用于什么场景?
- 如何将STM32配置为一个USB HID设备?
- 如何将STM32配置为一个USB CDC设备(虚拟串口)?
- 如何用软件模拟I2C时序?
一、STM32基础架构篇
1. STM32是什么?它基于什么架构? STM32是意法半导体(ST)推出的32位微控制器系列 ,基于ARM Cortex-M内核 的"豪华套装"。它把CPU、RAM、Flash、定时器、ADC、通信外设 全部集成在一个芯片里,是嵌入式领域的"瑞士军刀"。
架构:
-
内核 :ARM Cortex-M0/M0+/M3/M4/M7/M33(哈佛结构 ,Thumb-2指令集)
-
总线 :AHB (高性能总线,连内存)+ APB(外设总线,连UART/SPI)
-
存储 :Flash存程序 (0x0800 0000),RAM存数据(0x2000 0000)
底层真相 :STM32只是ARM内核的"装修公司",ST加了外设驱动和生态。
2. ARM Cortex-M0, M3, M4, M7内核的主要区别是什么?
表格
复制
| 内核 | 性能 | DSP | FPU | MPU | Cache | 应用场景 |
|---|---|---|---|---|---|---|
| M0 | 0.9 DMIPS/MHz | ❌ | ❌ | ❌ | ❌ | 超低功耗 ,简单控制 |
| M3 | 1.25 DMIPS/MHz | ❌ | ❌ | ✅ | ❌ | 平衡 ,主流 |
| M4 | 1.25 DMIPS/MHz | ✅ | 可选 | ✅ | ❌ | 需要DSP ,电机控制 |
| M7 | 2.14 DMIPS/MHz | ✅ | 双精度 | ✅ | I/D Cache | 高性能 ,GUI |
关键差异:
-
M4 :DSP指令 (单周期乘法累加MAC ),适合FFT、FIR
-
M7 :6级流水线 ,双精度FPU ,L1 Cache ,跑满400MHz
-
中断 :M0有4级优先级 ,M3/M4/M7有8-256级 (NVIC)
选型 :够用就好 ,M3覆盖80%场景 ,M7是性能怪兽。
3. 什么是CMSIS?它的作用是什么? CMSIS(Cortex Microcontroller Software Interface Standard) 是ARM定的 "嵌入式开发普通话" ,统一各厂商的接口。
作用:
-
硬件抽象 :core_cm3.h 定义NVIC、SysTick、SCB 寄存器,代码跨芯片
-
启动文件 :startup_stm32f103xe.s 统一中断向量表,**堆栈初始化 **
-
** DSP库 **: ** arm_math.h **, ** FFT、矩阵运算 , 优化过的汇编 **
-
** RTOS接口 **: ** CMSIS-RTOS **, ** 统一FreeRTOS、RTX **
** 底层 :CMSIS就是 寄存器定义+启动文件 , 没有它,每家厂商各说各话 **。
** 使用 : #include "stm32f1xx.h"** 间接包含cmsis_armcc.h ,直接用NVIC_EnableIRQ()。
4. 描述一下STM32从上电到开始执行main函数的过程 这是STM32的"** 蝶变 "过程, 从硬件到软件 **:
-
** 上电复位 :VDD稳定, NRST引脚拉高 , 时钟起振 **
-
** BOOT引脚采样 : BOOT0=0 , 从主Flash启动 **(0x0800 0000)
-
** 取中断向量表 :从
0x0800 0000读 初始栈指针SP ,从0x0800 0004读 Reset_Handler地址 ** -
** 执行Reset_Handler ( startup.s **):
-
** 初始化SP **
-
** 复制.data段**到RAM **(Flash→RAM)
-
** 清零.bss段 **(RAM初始0)
-
** 调用SystemInit()**配置时钟(72MHz)
-
** 调用__libc_init_array()**初始化C++静态对象
-
-
** 调用main() **:进入用户代码
** 关键 : Reset_Handler是C环境的"接生婆"**,**SystemInit是时钟的"调音师"**。
5. 什么是ISP和IAP?它们有何不同? ** ISP(In-System Programming) : 通过串口/USB下载 , 用系统Bootloader , 不依赖用户代码 **。
** IAP(In-Application Programming) : 用户程序自己写Flash , 自我更新 **。
** 对比 **:
表格
复制
| 特性 | ISP | IAP |
|---|---|---|
| ** 触发 ** | ** BOOT0=1 , 上电 ** | ** 程序运行时** |
| **代码 ** | ** ST内置Bootloader ** | ** 用户写Bootloader+APP** |
| 接口 | USART1/USB | 任意接口 (WiFi 、SD卡) |
| **安全性 ** | ** 低 , 谁都能下载 ** | ** 高 , 可加密验证 ** |
| 用途 | 开发调试 | **产品OTA升级 ** |
** ISP流程 **: ** BOOT0拉高+复位→STM32进入Bootloader→串口发0x7F握手→下载APP **
** IAP流程 **: ** APP收到OTA指令→下载固件到备份区→校验→改启动标志→复位→Bootloader搬运到运行区 **
** 注意**:** ISP会清空Flash**,** IAP要小心别跑飞**,**否则变砖**。
二、时钟与电源篇
6. STM32的供电引脚(VDD, VDDA, VBAT等)分别有什么作用?
-
VDD :数字电源 ,1.7V~3.6V ,给CPU、GPIO、外设供电
-
VDDA :模拟电源 ,给ADC、DAC、运放供电 ,必须<=VDD ,噪声要滤干净
-
VBAT :电池备份电源 ,1.8V~3.6V ,主电源断时给RTC、备份寄存器供电 ,电流<1μA
-
VREF+ :ADC参考电压 ,可接VDDA或外部精密源 ,决定ADC精度
-
VSS/VSSA :数字/模拟地 ,单点接地 ,防干扰
设计要点 :VDDA和VSSA之间接10nF+1μF电容 , PCB上走线要粗 。
7. 什么是时钟树?为什么它在STM32中很重要? 时钟树是"时钟的分配网络" ,像水厂的管网 ,把时钟源分频后送到各外设。
重要性:
-
性能 :72MHz主频 ,但UART要115200波特率 ,必须分频
-
功耗 :不用外设就关时钟 ,省电
-
同步 :ADC和TIM要同步 ,同根时钟源
-
灵活 :USB必须48MHz ,可PLL分频得到
STM32F103时钟树路径:
复制
HSE(8MHz) → PLL×9 → SYSCLK(72MHz)
↓
AHB(72MHz) → APB1(36MHz) → UART2/3/I2C
↓
APB2(72MHz) → UART1/SPI1/TIM1/ADC
配置 :SystemInit() 里设RCC_CFGR 寄存器,CubeMX图形化配。
8. 列举STM32的主要时钟源(HSI, HSE,LSI,LSE,PLL)。
-
HSI(High Speed Internal) :内部RC振荡器 ,8MHz ,精度±1% ,启动快 ,但温漂大
-
HSE(High Speed External) :外部晶振 ,4-16MHz ,精度高(±10ppm) ,PLL源首选
-
LSI(Low Speed Internal) :内部低速RC ,40kHz ,给IWDG/RTC ,精度差
-
LSE(Low Speed External) :32768Hz晶振 ,RTC专用 ,精度高
-
PLL(Phase-Locked Loop) :锁相环倍频 ,HSE×9=72MHz ,SYSCLK源
选型 :HSE+PLL=高性能 ,HSI=低成本 ,LSE=RTC必备。
9. 如何配置PLL以得到最高的系统时钟? STM32F103最高72MHz ,PLL倍频公式 :SYSCLK = HSE × PLLMUL
寄存器配置:
c
复制
// HSE=8MHz, PLLMUL=9, SYSCLK=72MHz
RCC->CR |= RCC_CR_HSEON; // 开HSE
while(!(RCC->CR & RCC_CR_HSERDY)); // 等稳定
RCC->CFGR &= ~RCC_CFGR_PLLMULL; // 清零
RCC->CFGR |= RCC_CFGR_PLLMULL9; // 9倍频
RCC->CFGR |= RCC_CFGR_PLLSRC; // PLL源=HSE
RCC->CR |= RCC_CR_PLLON; // 开PLL
while(!(RCC->CR & RCC_CR_PLLRDY)); // 等锁定
RCC->CFGR |= RCC_CFGR_SW_PLL; // SYSCLK=PLL
while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL); // 确认
CubeMX :输入HSE频率 ,选PLL倍频 ,自动算SYSCLK ,图形化配分频。
注意 :PLL输出必须<=72MHz ,超频会死机。
10. 什么是GPIO?STM32的GPIO有几种工作模式? GPIO(General Purpose Input/Output) 是通用输入输出引脚 ,STM32的"手脚"。
8种模式:
-
输入模式:
-
浮空输入 :不上下拉 ,电平不定 ,适合按键
-
上拉输入 :内部电阻拉VDD ,默认高
-
下拉输入 :内部电阻拉GND ,默认低
-
模拟输入 :连ADC ,禁用数字
-
-
输出模式:
-
推挽输出 :0/1都能推 ,驱动强 ,通用
-
开漏输出 :只能拉低 ,需外上拉 ,I2C用
-
-
复用功能 :推挽/开漏的变体 ,连UART/SPI
-
复用功能+上拉/下拉 :如USART_TX上拉,防悬空
配置:
c
复制
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
11. 推挽输出与开漏输出有什么区别?
表格
复制
| 特性 | 推挽(Push-Pull) | 开漏(Open-Drain) |
|---|---|---|
| ** 结构 ** | ** P-MOS+N-MOS , 都能导通 ** | ** 只有N-MOS , 只能拉低 ** |
| ** 输出 ** | ** 0和1都能输出 , 强驱动 ** | ** 只能输出0 , 1靠上拉 ** |
| ** 功耗 ** | ** 低 , 不耗电 ** | ** 高 , 上拉电阻耗电 ** |
| ** 速度 ** | ** 快 , 边沿陡 ** | ** 慢 , RC充电 ** |
| ** 应用 ** | ** LED 、 继电器 ** | ** I2C **、 ** 线与逻辑 ** |
** 底层 :推挽是 图腾柱结构 , 开漏是"开集电极"的CMOS版 **。
** I2C必须用开漏 ,因为 多设备"线与"仲裁**,**推挽会短路**。
12. 如何将一个GPIO引脚配置为上拉输入模式? 寄存器写法:
c
复制
// PA0上拉输入
GPIOA->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0); // 清零
GPIOA->CRL |= GPIO_CRL_CNF0_1; // 上拉/下拉模式
GPIOA->BSRR = GPIO_BSRR_BS0; // 上拉(BR0是下拉)
HAL库写法:
c
复制
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // IPU=Input Pull-Up
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
底层 :BSRR寄存器高16位BR复位 ,低16位BS置位 ,原子操作。
13. 什么是复用功能?如何将一个PA9引脚配置为USART1_TX? 复用功能(Alternate Function)是 "引脚的多路复用" ,一个引脚可当GPIO/UART/SPI。
AF映射表 :在Datasheet 的Alternate Function 章节查,PA9默认是USART1_TX。
配置:
c
复制
// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);
// 2. PA9复用推挽
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3. 配置USART1
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE);
底层 :AFIO_MAPR寄存器 配置复用映射 ,STM32F103默认映射 ,F103C8T6的PB6/PB7也可map到USART1。
14. 什么是NVIC?它在中断系统中起什么作用? NVIC(Nested Vectored Interrupt Controller) 是Cortex-M的"中断大管家" ,管理所有中断优先级和响应。
作用:
-
优先级管理 :8~256级可编程优先级 ,数值越小越优先
-
嵌套支持 :高优先级能打断低优先级ISR
-
向量表 :存储每个ISR入口地址 ,硬件自动跳转
-
中断使能/禁用 :ISER/ICER寄存器
-
挂起/清除 :ISPR/ICPR寄存器
寄存器操作:
c
复制
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; // USART1中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 抢占优先级0(最高)
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
底层 :NVIC在SCS(System Control Space)0xE000E000 ,系统空间。
15. STM32的中断优先级是如何分组和管理的? 优先级分组 是抢占优先级 vs 子优先级的划分。
分组值 :SCB->AIRCR的PRIGROUP[10:8] ,5种分法:
表格
复制
| 分组 | 抢占 | 子优先级 | 总级数 | 场景 |
|---|---|---|---|---|
| 0 | 0位 | 4位 | 16 | 无嵌套 ,全子优先级 |
| 1 | 1位 | 3位 | 2×8 | **1级抢占 ** |
| 2 | 2位 | 2位 | 4×4 | ** 平衡 ** |
| 3 | 3位 | 1位 | 8×2 | ** 多抢占 ** |
| 4 | 4位 | 0位 | 16 | ** 全抢占**,**无子优先级** |
配置:
c
复制
// 分组2:2位抢占,2位子优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 某中断:抢占=1,子=2
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
抢占 :打断别的ISR ,子优先级 :同抢占时排队。
推荐 :分组2(4×4) ,够用且灵活。
16. 什么是EXTI?它如何工作? EXTI(External Interrupt) 是外部中断/事件控制器 ,让GPIO能触发中断。
工作原理:
- 19条线 :线0~15 连GPIO Pin0~Pin15 (PA0/PB0...共用线0 ),线16=PDD , 线17=RTC闹钟 , **线18=USB唤醒 **
** 配置步骤**:
c
复制
// 1. 使能AFIO时钟(映射用)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
// 2. PA0映射到EXTI0
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
// 3. EXTI配置
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line0; // 线0
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; // 中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; // 上升沿
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
// 4. NVIC配置
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_Init(&NVIC_InitStructure);
** 注意 : PA0和PB0不能同时用EXTI0 **, ** 一次只能选一个 **。
** 17. SysTick定时器有什么用途? ** ** SysTick是Cortex-M内核的"送奶工" , 24位递减计数器 , 专给OS用 **。
** 用途 **:
-
** 系统节拍 **: ** FreeRTOS的心跳 **, ** 1ms中断一次 **,
xTaskIncrementTick() -
** 延时函数 **: ** HAL_Delay() ,
while(delay--)不精确, SysTick准 ** -
** 性能测试 **: ** 测代码执行时间 **,
start=SysTick->VAL; func(); end=SysTick->VAL;
** 配置 **:
c
复制
// 72MHz,1ms中断
SysTick_Config(SystemCoreClock / 1000); // 72000
void SysTick_Handler(void) {
HAL_IncTick(); // HAL的ms计数+1
xPortSysTickHandler(); // FreeRTOS节拍
}
** 寄存器 : SysTick在0xE000E010 , 内核空间 , 所有Cortex-M都有 **。
** 18. 看门狗定时器是什么?独立看门狗和窗口看门狗有何区别? ** ** 看门狗是"系统监工" , 程序跑飞时复位 **。
** 独立看门狗IWDG **:
-
** 独立RC时钟 **(40kHz), ** CPU停它也走 **
-
** 12位递减计数器 **, ** 喂狗写0xAAAA , 超0复位 **
-
** 简单 **, ** 防死循环 **, ** 时间窗固定 **
** 窗口看门狗WWDG **:
-
** APB1时钟 **, ** 7位递减 **, ** 可配时间窗 **
-
** 太早喂也复位 **, ** 防跑飞代码意外喂狗 **
-
** 有EWI中断 **( ** 计到0x40提前预警 **)
区别:
表格
复制
| 特性 | IWDG | WWDG |
|---|---|---|
| ** 时钟 ** | ** LSI独立 ** | ** PCLK1依赖 ** |
| ** 时间窗 ** | ** 无 ** | ** 有 **(太早太晚都复位) |
| ** 精度 ** | ** 低 **(RC不准) | ** 高 ** |
| ** 场景 ** | ** 终极防护 ** | ** 精确时间监控 ** |
配置:
c
复制
// IWDG:1秒超时
IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);
IWDG_SetPrescaler(IWDG_Prescaler_64); // 40kHz/64=625Hz
IWDG_SetReload(625); // 1秒
IWDG_ReloadCounter(); // 喂狗
IWDG_Enable();
// WWDG:100ms窗,50~100ms可喂
WWDG_Enable(0x7F); // 启动,计数=127
WWDG_SetPrescaler(WWDG_Prescaler_8); // 36MHz/8=4.5MHz
WWDG_SetWindowValue(0x5F); // 窗口值=95,约50ms后
WWDG_EnableIT(); // 开EWI中断
19. 如何从待机模式中唤醒STM32? 待机模式是最低功耗(2μA) ,只有RTC和备份寄存器活着。
唤醒源:
-
NRST复位 :按键复位
-
RTC闹钟 :RTC_ALR 或RTC_WAKEUP
-
WKUP引脚 :PA0(WKUP)上升沿
-
IWDG复位 :喂狗失败
配置:
c
复制
// 1. 使能PWR时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
// 2. 使能WKUP引脚(PA0)
PWR_WakeUpPinCmd(ENABLE);
// 3. 进入待机
PWR_EnterSTANDBYMode(); // 此函数不返回,唤醒后复位
RTC闹钟唤醒:
c
复制
// RTC配置闹钟
RTC_SetAlarm(RTC_GetCounter() + 10); // 10秒后
RTC_ITConfig(RTC_IT_ALR, ENABLE); // 使能闹钟中断
RTC_WaitForLastTask();
// 进待机
PWR_EnterSTANDBYMode();
注意 :待机唤醒=复位 ,PC从0开始 ,不是从WFI后面。
20. 什么是位带操作?它有什么优势? 位带是Cortex-M的"硬件外挂" ,把一个位映射成一个字 ,原子操作。
映射公式:
-
SRAM位带 :
0x2200 0000 + (字节地址-0x2000 0000)×32 + 位号×4 -
外设位带 :
0x4200 0000 + (字节地址-0x4000 0000)×32 + 位号×4
优势:
-
原子性 :读-改-写一气呵成 ,不被中断撕裂
-
简洁 :
BITBAND->PA_ODR8 = 1;比GPIOA->ODR |= (1<<9);安全 -
效率 :无锁 ,无临界区开销
代码:
c
复制
// 位带别名区定义
#define BITBAND_PERIPH(addr, bit) \
*(volatile uint32_t *)(PERIPH_BB_BASE + ((addr - PERIPH_BASE) << 5) + (bit << 2))
// 使用
#define LED_PIN 9
#define PA_ODR (&GPIOA->ODR)
BITBAND_PERIPH(PA_ODR, LED_PIN) = 1; // 亮
BITBAND_PERIPH(PA_ODR, LED_PIN) = 0; // 灭
注意 :Cortex-M0/M0+无位带 ,用LDREX/STREX模拟。
三、通信协议篇
21. UART通信的基本原理是什么? UART是 "异步串口" ,两根线(TX/RX) ,全双工, **"说"和"听"同时进行 **。
** 一帧格式 **:
复制
[起始位0] [数据位8位] [校验位] [停止位1] [空闲1]
1bit 8bits 0/1bit 1bit ...
** 原理 **:
-
** 波特率约定 : 双方约定速度 , 如115200 **
-
** 起始同步 : 起始位下降沿 , 接收方同步时钟 **
-
** 采样 : 16倍过采样 , 中间7-8-9三点投票 **
-
** 停止结束 **: ** 停止位高电平 , 帧间隔 **
** 底层 : TX移位寄存器→引脚 , RX引脚→移位寄存器 , UART_SR状态寄存器标志收发完成。
22. 如何配置UART以实现115200的波特率? 波特率公式 :UARTDIV = PCLK / (16 × 波特率)
PCLK:
-
USART1:APB2=72MHz
-
USART2/3:APB1=36MHz
计算 :USART1DIV = 72M / (16×115200) = 39.0625
-
DIV_Mantissa = 39(整数)
-
**DIV_Fraction = 0.0625×16 = 1 **(小数)
** 寄存器配置 **:
c
复制
// USART1, PCLK2=72MHz, 115200
USART1->BRR = (39 << 4) | 1; // 0x0271
USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE; // 使能+收发
HAL库:
c
复制
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE);
误差 :余数0.0625 ,误差0.16% ,<2%没问题。
23. UART如何通过中断方式接收不定长数据? ** 关键:接收空闲中断(IDLE)** + **DMA或RingBuffer **。
** 方案1:IDLE中断 **:
c
复制
void USART1_IRQHandler(void) {
if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) {
// 1. 清IDLE标志
uint16_t dummy = USART1->DR;
// 2. 读取接收到的长度
uint16_t len = UART_RX_BUF_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5);
// 3. 处理数据
process_uart_data(uart_rx_buf, len);
// 4. 重新启动DMA
DMA_Cmd(DMA1_Channel5, DISABLE);
DMA1_Channel5->CNDTR = UART_RX_BUF_SIZE;
DMA_Cmd(DMA1_Channel5, ENABLE);
}
}
// 启动时
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); // 使能IDLE中断
方案2:DMA+定时器:
- DMA收数据 ,定时器10ms超时 ,超时认为帧结束。
方案3:字符超时:
- 每收到一个字节重载定时器 ,超时则帧结束。
优势 :IDLE中断是STM32硬件特性 ,一帧结束自动触发 ,CPU负担轻。
24. 什么是DMA?它有什么好处? DMA(Direct Memory Access) 是 "数据搬运工" , **不占用CPU , 外设和内存直接传数据 **。
** 好处 **:
-
** 解放CPU : 传数据时CPU干别的 , 或多睡会儿 **
-
** 高速 : 速度=外设总线速度 , 不经过CPU **
-
** 连续 : 批量传输 , 不中断 **
** STM32F103 DMA **:
-
** 7通道 , 优先级可设 ( 高/中/低 **)
-
** 源/目的 : 外设→内存 、 内存→外设 、 内存→内存**
例子 :USART1->DR→RAM,收到100字节再中断 ,CPU不用每字节干预。
配置:
c
复制
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)uart_rx_buf;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 外设→内存
DMA_InitStructure.DMA_BufferSize = 100;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址固定
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 正常模式
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 非内存到内存
DMA_Init(DMA1_Channel5, &DMA_InitStructure);
DMA_Cmd(DMA1_Channel5, ENABLE);
25. 如何配置UART使用DMA进行数据发送和接收? 发送DMA:
c
复制
// 1. 使能DMA和USART时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 2. 配置DMA发送通道(USART1_TX是DMA1_Channel4)
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)tx_buffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // 内存→外设
DMA_InitStructure.DMA_BufferSize = len;
DMA_Init(DMA1_Channel4, &DMA_InitStructure);
// 3. 使能USART的DMA发送
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
// 4. 启动DMA
DMA_Cmd(DMA1_Channel4, ENABLE);
// 5. 等待发送完成
while(DMA_GetFlagStatus(DMA1_FLAG_TC4) == RESET);
DMA_ClearFlag(DMA1_FLAG_TC4);
接收DMA:
c
复制
// DMA1_Channel5是USART1_RX
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 外设→内存
DMA_InitStructure.DMA_BufferSize = RX_BUF_SIZE;
DMA_Init(DMA1_Channel5, &DMA_InitStructure);
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
DMA_Cmd(DMA1_Channel5, ENABLE);
关键 :DMA通道固定 ,USART1_TX=CH4,USART1_RX=CH5 ,查手册DMA请求映射表。
26. SPI有几种工作模式?由什么信号决定? 4种模式 ,由CPOL(时钟极性)和CPHA(时钟相位)决定。
表格
复制
| 模式 | CPOL | CPHA | 空闲电平 | 采样边沿 |
|---|---|---|---|---|
| 0 | 0 | 0 | 低 | ** ** 上升沿采样 ** |
| 1 | 0 | 1 | 低 | 下降沿采样 |
| 2 | 1 | 0 | 高 | 下降沿采样 |
| 3 | 1 | 1 | 高 | 上升沿采样 |
**配置 **:
c
复制
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 空闲低
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 第一个边沿采
// 模式0
** 由从设备决定 , 主设备必须匹配 。 W25Qxx Flash用模式0/3 , TFT屏模式0 **。
**27. 如何配置SPI为主机全双工模式? **
c
复制
// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE);
// 2. PA5/SCK, PA6/MISO, PA7/MOSI复用推挽
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // SCK/MOSI推挽
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // MISO上拉输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3. SPI配置
SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 主机
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 8位
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 模式0
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 软件片选
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; // 36MHz
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 高位先传
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
// 4. 软件片选
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; // PA4=CS
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA, GPIO_Pin_4); // CS高,不选中
收发:
c
复制
uint8_t SPI_RW(uint8_t data) {
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); // 等发送空
SPI_I2S_SendData(SPI1, data);
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); // 等接收非空
return SPI_I2S_ReceiveData(SPI1);
}
28. I2C通信的起始信号和停止信号是如何定义的? 起始(START) :SCL高电平时,SDA从高→低跳变 停止(STOP): **SCL高电平时,SDA从低→高跳变 **
复制
SCL ────┐ ┌─────────────
│ │
└──────────┘
SDA ─────┐ ┌─────
│ │
└─────┘
↑START ↑STOP
** 时序要求 : tSU;STA > 4.7μs ( 标准模式 ), 建立时间 **, ** SDA必须在SCL高前拉低 **。
** 软件模拟 **:
c
复制
void I2C_Start(void) {
I2C_SDA_H;
I2C_SCL_H;
delay_us(5);
I2C_SDA_L; // SCL高时SDA下降
delay_us(5);
I2C_SCL_L;
}
void I2C_Stop(void) {
I2C_SDA_L;
I2C_SCL_H;
delay_us(5);
I2C_SDA_H; // SCL高时SDA上升
delay_us(5);
}
** 注意 : 起始后SDA只准在SCL低时变 , 高时保持稳定 **。
**29. 如何用软件模拟I2C时序? ** ** 用GPIO模拟 **, ** 开漏+上拉 , SCL推挽 **。
** 代码框架**:
c
复制
// SCL推挽
#define I2C_SCL_H GPIO_SetBits(GPIOB, GPIO_Pin_6)
#define I2C_SCL_L GPIO_ResetBits(GPIOB, GPIO_Pin_6)
// SDA开漏,读取输入
#define I2C_SDA_H GPIO_SetBits(GPIOB, GPIO_Pin_7) // 释放,由上拉拉高
#define I2C_SDA_L GPIO_ResetBits(GPIOB, GPIO_Pin_7)
#define I2C_SDA_READ GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7)
void I2C_WBYTE(uint8_t byte) {
for(int i=0; i<8; i++) {
I2C_SCL_L;
if(byte & 0x80) I2C_SDA_H;
else I2C_SDA_L;
delay_us(5);
I2C_SCL_H; // SCL上升沿,从机采样
delay_us(5);
byte <<= 1;
}
I2C_SCL_L;
}
uint8_t I2C_RBYTE(void) {
uint8_t byte = 0;
for(int i=0; i<8; i++) {
I2C_SCL_H;
delay_us(5);
byte = (byte << 1) | I2C_SDA_READ; // 读SDA
I2C_SCL_L;
delay_us(5);
}
return byte;
}
优势 :硬件I2C有bug时(F103早期版) ,软件模拟更可靠。
** 30. I2C从机地址是如何组成的? ** 7位地址 = 4位固定 + 3位可配 (A0/A1/A2引脚)
** 格式**:
[7位地址 << 1] | [R/W位]
-
写 :
addr = (slave_addr << 1) | 0( **0xA0 **) -
** 读 :
addr = (slave_addr << 1) | 1( 0xA1 **)
** 例子 : AT24C02地址0xA0 **, ** A0/A1/A2接地 **, ** 7位=0b1010000 **。
** 10位地址 ( 扩展 **):
- ** 两字节 **:
11110 + 地址高2位 + R/W→地址低8位
** 配置 : 手册查地址 , 左移1位再或R/W **。
** 31. STM32的硬件I2C在应用时需要注意什么? ** F103的硬件I2C有"暗病" ,注意事项:
-
** 时钟配置 : APB1时钟必须>2MHz , 否则I2C死锁 **
-
** AFIO时钟 : 必须使能RCC_APB2ENR_AFIOEN , 否则无法映射 **
-
** 时序配置 : ** I2C clock speed , 标准100kHz , 快速400kHz ( CCR寄存器)
-
** 事件标志清除 **: ** 读SR1→读SR2 , 顺序错标志不清 **
-
** 死锁处理 **: ** BERR总线错误 , AF应答失败 , 必须软件复位I2C **
-
** DMA配合 **: ** 大数据用DMA **, ** CPU不阻塞 **
-
** 上拉电阻 **: ** 必须外接4.7kΩ **, ** 内部上拉弱 **
** 死锁复位**:
c
复制
void I2C_Reset(void) {
I2C_Cmd(I2C1, DISABLE);
RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, ENABLE); // 复位
RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, DISABLE);
I2C_Cmd(I2C1, ENABLE);
}
推荐 : **F103用硬件I2C , F4/H7用I2C+DMA , 稳定高效 **。
** 32. 比较UART、SPI和I2C三种通信协议的特点和适用场景。 **
表格
复制
| 协议 | 线数 | 速度 | 距离 | 主从 | 优点 | 缺点 | 场景 |
|---|---|---|---|---|---|---|---|
| ** UART ** | ** 2 (TX/RX) ** | ** 慢 **(115200bps) | ** 长 **(15m) | ** 无主 ** | ** 简单 、 全双工** | 异步、无流控易丢 | 调试、长距离 |
| SPI | 4+ (CLK/MOSI/MISO/CS) | 快(10Mbps) | 短(<1m) | 一主多从 | 全双工 、高速 | 费线 、无仲裁 | TFT屏、Flash |
| I2C | 2 (SDA/SCL) | 中(400kHz) | 短(<1m) | 多主 | 省线 、有仲裁 | 慢 、开漏上拉 | EEPROM、传感器 |
选型 :调试用UART ,高速用SPI ,省线用I2C。
33. 通用定时器有哪些主要功能? 通用定时器TIM2~TIM5 是 "瑞士军刀" ,功能:
-
定时/计数 :延时 、超时
-
PWM输出 :4通道 ,调电机/LED
-
输入捕获 :测脉冲宽度 、频率
-
输出比较 :翻转电平 、单脉冲
-
编码器接口 :电机测速
-
触发其他外设 :ADC/DAC
特性 :16位自动重装 、预分频1~65536 、4个独立通道。
**34. 如何配置定时器产生一个1kHz的PWM信号? ** ** 72MHz系统时钟 , 1kHz PWM **:
** 计算 **:
-
** 周期=1ms , 分频=72 , 计数值=1000 **
-
** PSC=72-1=71 , ARR=1000-1=999 **
** 代码 **:
c
复制
// TIM2通道1,PA0输出PWM
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Period = 999; // ARR
TIM_TimeBaseStructure.TIM_Prescaler = 71; // PSC
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // PWM模式1
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 500; // CCR=500,占空比50%
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
TIM_Cmd(TIM2, ENABLE);
占空比 :TIM_SetCompare1(TIM2, duty); 0~999。
35. PWM输出的频率和占空比由哪些寄存器决定? 频率 :ARR(自动重装载值) + PSC(预分频器) 占空比 :CCR(捕获比较值)
公式:
复制
PWM频率 = TIM_CLK / (PSC + 1) / (ARR + 1)
占空比 = CCR / (ARR + 1) × 100%
寄存器:
-
TIMx_PSC :预分频
-
TIMx_ARR :周期
-
TIMx_CCRy :通道y的比较值(y=1~4)
示例 :72MHz ,PSC=71 →1MHz ,ARR=999 →1kHz ,CCR=250 →25%占空比。
动态调:
c
复制
TIM_SetAutoreload(TIM2, 999); // 改频率
TIM_SetCompare1(TIM2, 250); // 改占空比
36. 如何用定时器捕获一个外部脉冲的高电平宽度? 输入捕获+从模式复位:
原理 :TI1FP1上升沿→捕获+复位计数器 ,下降沿→捕获CCR2 ,CCR2值=高电平时钟数。
配置:
c
复制
// TIM2 TI1连外部脉冲
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; // TI1
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; // 上升沿捕获
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICFilter = 0;
TIM_ICInit(TIM2, &TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2; // TI1FP2
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Falling; // 下降沿
TIM_ICInit(TIM2, &TIM_ICInitStructure);
// 从模式:复位模式,TI1FP1触发
TIM_SelectInputTrigger(TIM2, TIM_TS_TI1FP1);
TIM_SelectSlaveMode(TIM2, TIM_SlaveMode_Reset);
TIM_Cmd(TIM2, ENABLE);
读取:
c
复制
uint32_t high_time = TIM_GetCapture2(TIM2); // 高电平时间(时钟数)
float width_us = high_time / (SystemCoreClock / 1000000.0);
注意 :72MHz时钟,精度1μs ,捕获值16位,看是否溢出。
37. 什么是定时器的编码器接口模式?如何使用? 编码器模式 是定时器的"外挂" ,硬件解码正交编码器信号 (A/B相)。
原理 :A相上升/下降沿+B相电平 →计数器自动加减 ,不占用CPU。
配置:
c
复制
// TIM2 CH1=PA0 (A相), CH2=PA1 (B相)
TIM_EncoderInterfaceConfig(TIM2,
TIM_EncoderMode_TI12, // 两相都计数
TIM_ICPolarity_Rising, // A相上升沿
TIM_ICPolarity_Rising // B相上升沿
);
TIM_Cmd(TIM2, ENABLE);
// 读位置
int32_t position = TIM_GetCounter(TIM2); // 16位有符号
** 应用 **: ** 电机测速 、 位置反馈 、 机器人关节 **。
** 优势 : 硬件自动滤波 , 抗抖动 , 精度=编码器分辨率×4 **。
**38. 高级定时器(如TIM1)比通用定时器多了哪些功能? ** ** TIM1/TIM8是"豪华版" **:
-
** 重复计数器 : 更新N次才产生中断 , 适合步进电机 **
-
** 死区时间 : 互补PWM , 插入死区 , 防上下管直通 **
-
** 刹车功能 : 外部故障信号 , 立即关PWM , 保护电机 **
-
** 互补输出 : CH1和CH1N反相 , 驱动H桥 **
-
** 触发ADC : 多事件触发 , 同步采样 **
-
** 寄存器预装载 : 影子寄存器 **, ** 同步更新 **
** 场景 : 电机FOC控制 、 数字电源 、 需要保护的场合 **。
** 配置死区 **:
c
复制
TIM_BDTRInitTypeDef TIM_BDTRInitStructure;
TIM_BDTRInitStructure.TIM_OSSRState = TIM_OSSRState_Enable;
TIM_BDTRInitStructure.TIM_OSSIState = TIM_OSSIState_Enable;
TIM_BDTRInitStructure.TIM_DeadTime = 0xFF; // 死区时间
TIM_BDTRConfig(TIM1, &TIM_BDTRInitStructure);
39. 如何用定时器触发一个ADC转换? ADC外部触发, **TIMx_TRGO事件 **:
** 步骤 **:
-
** TIM配置为PWM模式 , 更新事件作为TRGO**
-
ADC配置为外部触发 ,TIMx_TRGO作为源
-
TIM启动,每周期自动触发ADC
**TIM配置 **:
c
复制
// TIM2 TRGO在更新时产生
TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update);
** ADC配置 **:
c
复制
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_TRGO; // TIM2触发
ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_Rising; // 上升沿
ADC_Init(ADC1, &ADC_InitStructure);
ADC_Cmd(ADC1, ENABLE);
** 应用 : 固定频率采样 , 电机电流采样 , 音频采集 **。
**40. ADC的分辨率是什么意思?STM32的ADC通常是多少位? ** ** 分辨率 = ADC能分辨的最小电压 **,Vmin = Vref / 2^n。
STM32:
-
F103 :12位( **0~4095 **)
-
** F4/H7 : 12/16位可选 **
-
** L4 : 12位 **
** 精度 ≠ 分辨率 **: ** 分辨率是理论值 , ** 精度看参考电压噪声。
** 计算 **:
c
复制
// Vref=3.3V,12位,测得2048
float voltage = 2048 * 3.3 / 4096; // = 1.65V
** 优势 : 12位比10位(1024)精细4倍 , 能测0.8mV变化 **。
41. 什么是ADC的规则通道和注入通道? ** 规则通道 = 常规采样 **, ** 按顺序扫 **, ** 最多16个通道 。 注入通道 = 紧急采样 **, ** 可插队 , ** 最多4个。
** 类比 : 规则=普通门诊 **, ** 注入=急诊 , 注入优先级高 **。
** 配置 **:
c
复制
// 规则通道:CH1, CH2, CH3扫描
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 2, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 3, ADC_SampleTime_55Cycles5);
// 注入通道:CH4(紧急)
ADC_InjectedChannelConfig(ADC1, ADC_Channel_4, 1, ADC_SampleTime_13Cycles5);
ADC_InjectedSequencerLengthConfig(ADC1, 1); // 1个注入通道
触发 :规则由软件/SYSCLK/TIM触发 ,注入可TIM/外部引脚触发。
42. 如何实现多通道ADC扫描转换? ** 步骤 **:
-
** 使能扫描模式 **:
ADC_InitStructure.ADC_ScanConvMode = ENABLE; -
** 配置多个规则通道 **: **
ADC_RegularChannelConfig(), 指定序号 ** -
** 使能连续转换**:
ADC_ContinuousConvModeCmd(ADC1, ENABLE); -
DMA搬运:**转换完自动填数组 **
** 代码 **:
c
复制
#define ADC_CH_NUM 3
uint16_t adc_values[ADC_CH_NUM];
// 配置3个通道
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 2, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 3, ADC_SampleTime_55Cycles5);
// DMA配置
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; // 源固定
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_values;
DMA_InitStructure.DMA_BufferSize = ADC_CH_NUM;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 目标递增
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
// ADC DMA使能
ADC_DMACmd(ADC1, ENABLE);
ADC_Cmd(ADC1, ENABLE);
ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 启动
结果 :adc_values[0]=CH1 ,adc_values[1]=CH2 ,adc_values[2]=CH3 ,循环更新。
43. ADC的采样时间如何影响转换结果? 采样时间 = **开关接通时间 , 让采样电容充电到输入电压 **。
** 公式 :Tconv = 采样时间 + 12.5周期( 12位转换需要**)
影响:
-
** 采样太短 : 电容没充满 , 读值偏小 , 误差大 **
-
** 采样太长 : 转换慢 , 影响吞吐率 **
** 选择 **:
-
** 低阻抗源 **( ** <1kΩ **) : ** 1.5周期 ( 0.2μs **)
-
** 高阻抗源 ( 10kΩ **) : ** 55.5周期 ( 7.7μs **)
-
** 内部温度传感器 : 55.5周期 ( 推荐 **)
** 配置 **:
c
复制
ADC_InitStructure.ADC_ScanConvMode = ENABLE;
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_55Cycles5);
** 底层 : ADC_SMPR1/SMPR2**寄存器,**3位/通道**。
44. 如何校准ADC? 校准 =消除内部电容失调 ,上电后必须做。
步骤:
c
复制
// 1. 等待ADC稳定
ADC_Cmd(ADC1, ENABLE);
delay_ms(2);
// 2. 启动校准
ADC_ResetCalibration(ADC1); // 复位校准
while(ADC_GetResetCalibrationStatus(ADC1)); // 等完成
ADC_StartCalibration(ADC1); // 开始校准
while(ADC_GetCalibrationStatus(ADC1)); // 等完成
// 3. 校准完成,可以转换
**校准值 **: ** ADC_DR 寄存器 读回校准结果 , ST生产时校准过 , 用户可不复用 **。
** 注意 : 每次上电校准一次 , 温度变化大时重新校准 **。
45. DAC的主要用途是什么? DAC =**数模转换器 **, ** 数字→模拟电压 , 输出连续信号 **。
用途:
-
信号发生器 :正弦波 、三角波 、方波
-
音频输出 :播放语音
-
控制电压 :调电机转速 、LED亮度
-
测试 :给外部电路提供参考电压
STM32F103 :2个DAC通道 ,12位 ,速度1Msps。
配置:
c
复制
DAC_InitTypeDef DAC_InitStructure;
DAC_InitStructure.DAC_Trigger = DAC_Trigger_Software; // 软件触发
DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None;
DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Enable; // 使能输出缓冲
DAC_Init(DAC_Channel_1, &DAC_InitStructure);
DAC_Cmd(DAC_Channel_1, ENABLE);
// 输出电压
DAC_SetChannel1Data(DAC_Align_12b_R, 2048); // 3.3V/2=1.65V
注意 :DAC输出是PA4/PA5 ,负载能力 <5mA。
46. 比较器的工作原理是什么? 比较器 =**1位ADC **, ** 比较两电压 , 输出0/1 **。
** 输入 **: ** IN+(正) , IN-(负) , 输出 **: ** OUT **
** 原理 : IN+ > IN- → OUT=1(VDD) , IN+ < IN- → OUT=0(GND) **
** STM32比较器 ( F3/L4有 , F103无 **):
-
** 迟滞 **: ** 防振荡 , IN+在IN-附近抖动时不跳变 **
-
** 滤波 **: ** 数字滤波 , 去毛刺 **
-
**唤醒 : 输出变1唤醒CPU **
应用 :过零检测 、电源掉电检测 、快速保护 (比ADC快)。
47. 什么是实时时钟?如何配置RTC并产生一个闹钟中断? RTC(Real-Time Clock) 是 "万年历" , 掉电仍走 (VBAT供电)。
功能 :秒计数 、闹钟 、** wakeup 、 入侵检测 **。
** 配置 **:
c
复制
// 1. 使能PWR和BKP时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
// 2. 允许访问备份寄存器
PWR_BackupAccessCmd(ENABLE);
// 3. 配置LSE
RCC_LSEConfig(RCC_LSE_ON);
while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET); // 等LSE稳定
// 4. RTC时钟源=LSE
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE);
// 5. 等RTC同步
RTC_WaitForSynchro();
// 6. 配置闹钟
RTC_SetAlarm(RTC_GetCounter() + 10); // 10秒后闹钟
RTC_ITConfig(RTC_IT_ALR, ENABLE);
// 7. NVIC
NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn;
NVIC_Init(&NVIC_InitStructure);
void RTC_IRQHandler(void) {
if(RTC_GetITStatus(RTC_IT_ALR) != RESET) {
RTC_ClearITPendingBit(RTC_IT_ALR);
printf("Alarm!\r\n");
}
}
时间设置 :RTC_SetCounter(Unix时间),1970年1月1日0点起秒数。
48. 如何将数据备份到RTC的备份寄存器中? 备份寄存器BKP =20个16位寄存器 ,VBAT供电 ,掉电保存。
用途 :存用户配置 、校准参数。
代码:
c
复制
// 1. 使能时钟+访问
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);
// 2. 写入
BKP_WriteBackupRegister(BKP_DR1, 0x1234); // DR1存0x1234
// 3. 读取
uint16_t data = BKP_ReadBackupRegister(BKP_DR1);
注意 :主电源断后,VBAT必须供电 ,否则数据丢失。
49. 芯片唯一ID有什么用途? 唯一ID=96位 ,工厂烧录 ,全球唯一 ,地址0x1FFFF7E8。
用途:
-
** 产品序列号 : ** 设备身份证
-
加密 :ID+密钥 →生成许可证
-
防克隆 :ID不匹配不运行
-
追踪 :生产/维修记录
读取:
c
复制
uint16_t *uid = (uint16_t *)0x1FFFF7E8;
printf("UID: %04X %04X %04X\r\n", uid[0], uid[1], uid[2]);
注意 :ID不是随机数 ,可预测 ,加密要加盐。
50. Flash存储器是如何组织的?什么是页,什么是扇区? STM32F103 Flash=512KB=64页×2KB(每页)。
页(Page) :最小擦除单位 ,2KB(中容量) ,擦除时间20ms。
扇区(Sector) :大容量芯片(F103ZE)=128KB×4扇区 ,擦除时间1s。
**组织 **:
复制
地址 内容
0x0800 0000 页0(2KB)
0x0800 0800 页1
...
0x0807 F800 页63
** 选项字节**:** 0x1FFFF800**,**配置读保护、写保护、用户数据**。
注意 :Flash寿命10万次 ,频繁写要磨损均衡。
51. 如何对内部Flash进行读写操作? **步骤:解锁→擦→写→锁 **。
** 代码 **:
c
复制
// 1. 解锁Flash
FLASH_Unlock();
// 2. 擦除页(必须整页)
FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
FLASH_ErasePage(0x08008000); // 擦页1
// 3. 写半字(16位)
FLASH_ProgramHalfWord(0x08008000, 0x1234); // 地址必须2字节对齐
// 4. 上锁
FLASH_Lock();
// 5. 读取
uint16_t data = *(uint16_t *)0x08008000;
注意 :写前必须擦 ,擦后全0xFF ,只能写0 ,不能写1。
52. 什么是选项字节?如何配置它? 选项字节 =**Flash的"配置页" , 存读保护、写保护、看门狗模式、用户数据 **。
** 地址 **: ** 0x1FFFF800 ( OB_RDR **), ** 16字节 **。
** 位定义 **:
-
** RDP **: ** 读保护 , ** 0xA5=不保护 ,
0xXX=保护 -
USER :看门狗、复位模式
-
WRP0~WRP3 :写保护 ,每4页1位
配置:
c
复制
// 1. 解锁OB
FLASH_Unlock();
FLASH_ClearFlag(FLASH_FLAG_OPTVERR);
// 2. 解锁选项字节
FLASH->OPTKEYR = 0x45670123; // KEY1
FLASH->OPTKEYR = 0xCDEF89AB; // KEY2
// 3. 修改OB
FLASH->OB_RDR = 0xA5; // 解除读保护
// 4. 启动OB修改
FLASH->CR |= FLASH_CR_OPTSTRT;
while(FLASH->CR & FLASH_CR_OPTSTRT); // 等完成
// 5. 复位生效
NVIC_SystemReset();
**CubeProgrammer : 图形化改OB , 简单安全 **。
** 53. 在STM32中如何实现一个简单的引导程序?** **Bootloader=小系统+搬APP **。
** 设计 **:
-
** 启动判断 : ** 按键/标志 →Bootloader or APP
-
通信 :USART/YModem协议 →接收固件
-
写Flash :按页擦写
-
跳转APP :关中断→设SP→设PC =
APP地址+4
代码:
c
复制
// 1. 启动判断
if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0) { // 按键按下
run_bootloader();
} else {
jump_to_app();
}
// 2. 跳转APP
typedef void (*pAppEntry)(void);
void jump_to_app(void) {
uint32_t app_addr = 0x08004000; // APP在页2
// 检查栈顶合法性(RAM范围)
if(((*(uint32_t *)app_addr) & 0x2FFE0000) == 0x20000000) {
__disable_irq(); // 关中断
SCB->VTOR = app_addr; // 重定位向量表(F4+)
pAppEntry app_entry = (pAppEntry)*(uint32_t *)(app_addr + 4);
__set_MSP(*(uint32_t *)app_addr); // 设主栈指针
app_entry(); // 跳转
}
}
// 3. YModem接收
void ymodem_recv(void) {
uint8_t buf[1024];
while(1) {
uart_recv(buf, 1024);
if(buf[0] == SOH) { // 开始头
FLASH_ProgramPage(addr, buf+3);
addr += 1024;
}
}
}
** 协议 : YModem , CRC校验**,**断点续传**。
54. 如何将程序从内部Flash搬运到外部RAM中运行以提高速度? F103无外部RAM ,F4/H7有FSMC/FMC。
步骤:
-
** 链接脚本 **: **
.text段放外部RAM , 启动文件搬运 ** -
** 启动文件 : ** 复制代码到外部RAM , 跳转执行
** LD文件**:
ld
复制
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
EXT_RAM (rxw) : ORIGIN = 0x60000000, LENGTH = 8M // FSMC地址
}
SECTIONS
{
.ext_code :
{
*(.ext_text*)
} > EXT_RAM AT > FLASH // 运行时载外部RAM,镜像存Flash
}
搬运:
c
复制
// 启动时
void CopyExtCode(void) {
extern uint32_t _siext_text, _sext_text, _svextext;
memcpy(&_sext_text, &_siext_text, &_sext_text - &_svextext);
}
** 实际 : 内部Flash 72MHz足够 , 外部RAM慢 , 一般只放数据 , 不放代码 **。
** 55. 什么是内存管理单元?哪些Cortex-M内核拥有它? ** ** MMU(Memory Management Unit) **= ** 虚拟内存 **, ** 地址转换 **, ** Linux需要 **。
** Cortex-M : 无MMU , 只有MPU(Memory Protection Unit) ( M3/M4/M7/M33有 **)。
MPU =**内存保护 **, ** 分8区 **, ** 设权限 , 防越界 **。
** MMU vs MPU **:
-
** MMU : 虚拟→物理 **, ** 页表 **, ** 支持Linux **(Cortex-A)
-
** MPU : 物理保护 **, ** 无虚拟 **, ** 裸机/RTOS **
** STM32 : MPU **, ** 配置见问题57 **。
56. 如何配置MPU以保护不同的内存区域? **MPU=系统保镖 **。
** 配置 **:
c
复制
MPU_Region_InitTypeDef MPU_InitStruct;
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x20000000; // RAM
MPU_InitStruct.Size = MPU_REGION_SIZE_128KB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; // 全访问
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.Number = 0; // 区0
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; // 可执行
HAL_MPU_ConfigRegion(&MPU_InitStruct);
// 使能MPU
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
** 栈保护 **: ** 栈底设MPU区 **, ** 大小0KB , 访问违规触发Fault **。
**57. 什么是DSP指令集?哪些STM32系列支持它? ** ** DSP指令 **= ** ARM的"数学加速包" , 单指令多数据 **。
**指令 **:
-
** SMLABB/SMLABT : 16×16+32累加 **, ** MAC ( 乘累加 **)
-
** SMULxy : 16×16→32 , 滤波**
-
** MLA** = **1周期乘加 **, ** C语言几十周期 **
** 支持 **:
-
** Cortex-M4/M7/M33 **: ** 全DSP **
-
** 系列**:** F4/H7/G4**,** 标志=**
__ARM_FEATURE_DSP
使用:
c
复制
#include "arm_math.h"
// FFT要用DSP库
arm_rfft_fast_instance_f32 fft_inst;
arm_rfft_fast_init_f32(&fft_inst, 1024);
arm_rfft_fast_f32(&fft_inst, input, output, 0); // 硬件加速
效果 :**FFT提速3-5倍 , 电机FOC必备 **。
58. 如何用STM32实现一个简单的FFT? **步骤 **:
-
** 采集数据 : ADC采样1024点 **
-
** 加窗 : 汉宁窗 **, ** 防频谱泄露 **
-
** 调用DSP库 **: ** arm_rfft_fast_f32 **
-
** 算幅值 : sqrt(real²+imag²)**
代码:
c
复制
#define FFT_SIZE 1024
float32_t input[FFT_SIZE];
float32_t output[FFT_SIZE];
float32_t fft_mag[FFT_SIZE/2];
// 1. ADC采样填充input
for(int i=0; i<FFT_SIZE; i++) {
input[i] = (float32_t)read_adc();
}
// 2. 加汉宁窗
for(int i=0; i<FFT_SIZE; i++) {
input[i] *= 0.5 * (1 - cosf(2*PI*i/(FFT_SIZE-1)));
}
// 3. FFT
arm_rfft_fast_instance_f32 fft_inst;
arm_rfft_fast_init_f32(&fft_inst, FFT_SIZE);
arm_rfft_fast_f32(&fft_inst, input, output, 0);
// 4. 算幅值
arm_cmplx_mag_f32(output, fft_mag, FFT_SIZE/2);
// 5. 找峰值频率
uint32_t max_index = 0;
arm_max_f32(fft_mag, FFT_SIZE/2, &max_val, &max_index);
float freq = max_index * Fs / FFT_SIZE; // Fs=采样率
资源 :F103 M3无DSP,FFT慢 ,F4/H7用DSP快。
59. 浮点单元是什么?哪些STM32系列拥有硬件FPU? FPU(Floating Point Unit)=硬件浮点加速器 ,单精度/双精度指令。
支持:
-
Cortex-M4F/M7 :单精度FPU (float)
-
Cortex-M7 :双精度FPU (double)
-
系列 :F4/H7/G4/L4+
使用:
c
复制
// 编译选项:-mfloat-abi=hard -mfpu=fpv4-sp-d16
float a = 1.23f, b = 4.56f;
float c = a * b; // 单周期完成,不用软件库
性能 :F103软件浮点=100周期 ,F4硬件=1周期,**快100倍 **。
60. 使用FPU需要注意什么? 注意:
-
** 编译选项 **: ** makefile **, ** -mfloat-abi=hard -mfpu=fpv4-sp-d16 **
-
** 中断保存 : ** FPU寄存器 **S0-S31 **, ** 进出中断要压栈 **( ** 自动 , FPCCR寄存器 **)
-
** RTOS : 任务切换要保存FPU上下文 , FreeRTOS需配置**
c
复制
// FreeRTOSConfig.h #define configUSE_TASK_FPU_SUPPORT 1 -
功耗 :FPU开=+5mA ,不用时关
c
复制
SCB->CPACR |= (0xF << 20); // 使能FPU
陷阱 :混用hard和soft float ,ABI不兼容 ,链接失败。
**61. 以太网MAC和PHY的区别是什么? **
-
** MAC(Media Access Control) **: ** 数据链路层 **, ** 帧收发 、 CRC校验 、 DMA **, ** STM32内置 **
-
** PHY(Physical Layer) **: ** 物理层 **, ** 模拟信号 、 编码 、 RJ45接口 **, ** 外部芯片(如DP83848) **
** 关系 : MAC→MII/RMII接口→PHY→网线 **。
**配置 **: ** PHY地址 **( ** 0/1 ) , 自动协商 Speed/Duplex **。
**62. 如何配置LWIP协议栈以实现TCP通信? ** ** LWIP=LightWeight IP **, ** 嵌入式TCP/IP **。
** 步骤**:
-
CubeMX :** 使能ETH , 配置PHY , 选LWIP **
-
** 代码**:
c
复制
// 初始化 MX_LWIP_Init(); // 创建TCP服务器 struct tcp_pcb *pcb = tcp_new(); tcp_bind(pcb, IP_ADDR_ANY, 8080); pcb = tcp_listen(pcb); tcp_accept(pcb, tcp_accept_callback); -
** 回调**:
c
复制
err_t tcp_accept_callback(void *arg, struct tcp_pcb *newpcb, err_t err) { tcp_recv(newpcb, tcp_recv_callback); return ERR_OK; }
内存:**F103不够用 **, ** F4/H7推荐 **, ** RAM至少64KB **。
63. USB有哪几种传输类型?各自适用于什么场景?
表格
复制
| 类型 | 方向 | 速度 | 可靠性 | 场景 |
|---|---|---|---|---|
| ** 控制 **(Control) | 双向 | ** 满速 ** | ** 高 ** | ** 枚举 ** 、配置 |
| 批量(Bulk) | 单向 | ** 慢/高速 ** | ** 高** | U盘 、打印机 |
| 中断(Interrupt) | 单向 | ** 满速 ** | ** 高 ** | HID 、鼠标键盘 |
| 等时(Isochronous) | 单向 | ** 高速 ** | ** 低 ** | ** 音频 ** 、视频 |
批量 :带宽大 ,可重传 。等时 :实时性强 ,丢包不重传。
**64. 如何将STM32配置为一个USB HID设备? ** ** HID(Human Interface Device) **= ** 免驱 **, ** 鼠标键盘 **。
** 步骤 **:
-
** CubeMX **: ** USB_DEVICE→HID **, ** 生成 **
-
** 报告描述符 **: ** 定义数据格式 , 8字节报表 **
-
** 发送数据 **:
c
复制
uint8_t hid_data[8] = {0}; // x, y, buttons hid_data[1] = 10; // x移动 USBD_HID_SendReport(&hUsbDeviceFS, hid_data, 8);
报表描述符:
c
复制
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x02, // Usage (Mouse)
0xA1, 0x01, // Collection (Application)
0x09, 0x01, // Usage (Pointer)
0xA1, 0x00, // Collection (Physical)
0x05, 0x09, // Usage Page (Buttons)
0x19, 0x01, // Usage Minimum (0x01)
0x29, 0x03, // Usage Maximum (0x03)
优势 :无需驱动 ,Windows/Linux即插即用。
**65. 如何将STM32配置为一个USB CDC设备(虚拟串口)? ** ** CDC(Communication Device Class) **= ** 虚拟COM **, ** 电脑认作串口 **。
** 步骤**:
-
CubeMX :** USB_DEVICE→CDC , 生成 **
-
** 代码自动 : ** USBD_CDC_SetTxBuffer() , USBD_CDC_TransmitPacket()
-
接收: ** CDC_Receive_FS()回调 **
发送:
c
复制
uint8_t data[] = "Hello USB\r\n";
USBD_CDC_SetTxBuffer(&hUsbDeviceFS, data, strlen(data));
USBD_CDC_TransmitPacket(&hUsbDeviceFS);
应用 : **USB转串口 , 调试神器 , 速度12Mbps **。
** 66. CAN总线的基本帧、扩展帧和远程帧有什么区别? **
表格
复制
| 帧类型 | ID位数 | 用途 | 帧格式 |
|---|---|---|---|
| ** 基本帧 ** | ** 11位 ** | ** 标准通信 ** | ** 0x000~0x7FF ** |
| ** 扩展帧 ** | ** 29位 ** | ** 大量设备 ** | ** 0x00000000~0x1FFFFFFF ** |
| ** 远程帧 ** | ** 11/29位 ** | ** 请求数据 ** | ** RTR=1 , 无数据场 ** |
扩展帧 :**ID=29位=11位基ID+18位扩展ID , IDE位=1 **。
** 远程帧 **: ** 无数据 , 帧类型RTR=1 , 请求对方发数据 **。
** 仲裁 : 基本帧优先级>扩展帧 ( ID小优先 **)。
67. 如何配置CAN总线过滤器? **过滤器=硬件分拣器 **, ** 只收关心的ID **。
模式:
-
** 列表模式 : 精确匹配 , 收4个ID **
-
** 掩码模式 : 模糊匹配 , 收一类ID **
**代码 **:
c
复制
CAN_FilterInitTypeDef CAN_FilterInitStructure;
CAN_FilterInitStructure.CAN_FilterIdHigh = 0x123 << 5; // ID左移5位
CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x7FF << 5; // 掩码:所有位匹配
CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask; // 掩码模式
CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_16bit; // 16位
CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
CAN_FilterInit(&CAN_FilterInitStructure);
**收0x100~0x1FF一类ID **:
复制
Mask = 0x700 // 高3位关心
Id = 0x100 // 基地址
**68. 如何计算CAN总线的波特率? ** ** 波特率公式 **: Bitrate = APB1CLK / (BRP × (1 + BS1 + BS2))
** 参数 **:
-
**BRP(波特率预分频) **: ** 1~1024 **
-
** BS1(时间段1) **: ** 1~16Tq **
-
** BS2(时间段2) **: ** 1~8Tq **
-
** 采样点 **= ** (1 + BS1) / (1 + BS1 + BS2) , 推荐85% **
** 计算 : APB1=36MHz , 目标250kbps **:
复制
BRP=9, BS1=8, BS2=3
Bitrate = 36M / (9 × (1+8+3)) = 36M / 108 = 250k
** 配置 **:
c
复制
CAN_InitStructure.CAN_BS1 = CAN_BS1_8tq;
CAN_InitStructure.CAN_BS2 = CAN_BS2_3tq;
CAN_InitStructure.CAN_Prescaler = 9;
**69. SDIO接口和SPI模式驱动SD卡有何区别? **
表格
复制
| 特性 | SDIO(4位) | SPI(1位) |
|---|---|---|
| ** 速度 ** | ** 快 ( 48MHz **) | ** 慢 ( 20MHz **) |
| ** 引脚 ** | ** CLK/CMD/D0-D3 ** | ** CLK/MOSI/MISO/CS ** |
| ** 协议 ** | ** SDIO协议 ** | ** SPI协议 ** |
| ** CPU ** | ** DMA ** | 中断/DMA |
| ** 功耗 ** | ** 高 ** | ** 低 ** |
| ** 适用 ** | ** 高速 ** | ** 低速/省线 ** |
SDIO:**F4/H7有 **, ** F103无 **, ** 用SPI模式 **。
**70. 如何通过FSMC接口驱动TFT液晶屏? **
继续你的回答
编辑
复制
分享
STM32关键技术问答汇总
- ** 什么是LTDC?它有什么作用? ** ** LTDC(LCD-TFT Display Controller) 是 STM32F4/H7的"显卡" , 直接驱动RGB接口的液晶屏 **, ** 无需SPI/I2C **。
** 作用 **:
-
** 并口 **: ** 24位RGB数据线 , 时钟 , HSYNC , VSYNC **, ** DE **
-
** 显存 **: ** 在RAM中划一块 framebuffer **, ** LTDC自动刷 **
-
** 图层 **: ** 2层叠加 **, ** 透明度混合 **, ** 硬件加速 **
-
** 分辨率 **: ** 最大1024×768 **( ** H7可到4K **)
** 配置 **:
c
复制
// LTDC时序配置(800×480屏)
LTDC_InitTypeDef LTDC_InitStruct;
LTDC_InitStruct.LTDC_HSPolarity = LTDC_HSPolarity_AL; // HSYNC低有效
LTDC_InitStruct.LTDC_VSPolarity = LTDC_VSPolarity_AL;
LTDC_InitStruct.LTDC_DEPolarity = LTDC_DEPolarity_AL;
LTDC_InitStruct.LTDC_PCPolarity = LTDC_PCPolarity_IPC;
LTDC_InitStruct.LTDC_HorizontalSync = 48 - 1;
LTDC_InitStruct.LTDC_VerticalSync = 3 - 1;
LTDC_InitStruct.LTDC_AccumulatedHBP = 48 + 88 - 1;
LTDC_InitStruct.LTDC_AccumulatedVBP = 3 + 32 - 1;
LTDC_InitStruct.LTDC_AccumulatedActiveW = 48 + 88 + 800 - 1;
LTDC_InitStruct.LTDC_AccumulatedActiveH = 3 + 32 + 480 - 1;
LTDC_InitStruct.LTDC_TotalWidth = 48 + 88 + 800 + 40 - 1;
LTDC_InitStruct.LTDC_TotalHeigh = 3 + 32 + 480 + 13 - 1;
LTDC_Init(<DC_InitStruct);
** 显存 **: ** 在SDRAM中划800×480×4字节(RGB888) **。
72. DCMI接口用于什么场景? ** DCMI(Digital Camera Interface) 是 STM32的"摄像头接口" **, ** 并口 **, ** 8/10/12/14位数据线 **。
** 场景 **: ** 接摄像头 **( ** OV2640 、 OV5640 **), ** 图像采集 , 条码扫描 , 机器视觉 **。
** 信号**:** PIXCLK像素时钟 , VSYNC帧同步 , HSYNC行同步 , DATA[0..13] **。
**配置 **:
c
复制
DCMI_InitTypeDef DCMI_InitStructure;
DCMI_InitStructure.DCMI_CaptureMode = DCMI_CaptureMode_Continuous; // 连续采集
DCMI_InitStructure.DCMI_SynchroMode = DCMI_SynchroMode_Hardware; // 硬件同步
DCMI_InitStructure.DCMI_PCKPolarity = DCMI_PCKPolarity_Rising; // 上升沿采样
DCMI_InitStructure.DCMI_HSPolarity = DCMI_HSPolarity_Low;
DCMI_InitStructure.DCMI_VSPolarity = DCMI_VSPolarity_Low;
DCMI_InitStructure.DCMI_DataExtendedMode = DCMI_DataExtendedMode_8b; // 8位数据
DCMI_Init(&DCMI_InitStructure);
DMA :DCMI+DMA2, ** 直接存SDRAM **, ** 不占用CPU **。
73. 如何用STM32驱动一个WS2812B LED? ** ** WS2812B="时序怪物" ,**0/1靠400ns/800ns脉宽区分 , 必须单周期精度 **。
**方案1:GPIO+NOP延时 ( F103够用 **):
c
复制
#define NOP __NOP()
void ws2812_send_byte(uint8_t byte) {
for(int i=0; i<8; i++) {
if(byte & 0x80) { // 1码:800ns高
GPIO_SetBits(GPIOA, GPIO_Pin_0);
NOP; NOP; NOP; // 12个NOP≈800ns@72MHz
// 共15个NOP
GPIO_ResetBits(GPIOA, GPIO_Pin_0);
} else { // 0码:400ns高
GPIO_SetBits(GPIOA, GPIO_Pin_0);
NOP; NOP; // 6个NOP≈400ns
GPIO_ResetBits(GPIOA, GPIO_Pin_0);
}
byte <<= 1;
}
}
方案2:SPI DMA:**SPI波特率=2.4MHz **, ** 00111100B=1码 **, ** 00011100B=0码 **。
** 方案3:PWM+DMA **: ** TIM2 PWM **, ** DMA更新CCR , 自动序列 **。
** 要点 **: ** 关中断 , 防时序被打断 **。
**74. 如何读取旋转编码器的值? ** ** 编码器=正交AB相 , 用定时器编码器模式 **。
** 电路 **: ** A相接TIM2_CH1 , B相接TIM2_CH2 **。
代码:
c
复制
// TIM2编码器模式
TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12,
TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
TIM_Cmd(TIM2, ENABLE);
// 读值(有符号)
int16_t encoder_val = TIM_GetCounter(TIM2); // 正转+1,反转-1
// 清0
TIM_SetCounter(TIM2, 0);
** 防抖 **: ** 硬件编码器模式自动滤毛刺 **, ** 或用GPIO中断+状态机 **。
** 应用 **: ** 音量调节 、 菜单选择 、 电机位置反馈 **。
75. 如何通过STM32驱动一个步进电机? **步进电机=脉冲驱动 **, ** 每脉冲转1.8° **。
** 驱动方式 **:
-
** GPIO直接驱动 **: ** 电流<20mA **, ** 只能驱微型步进 **
-
** ULN2003 **: ** 达林顿管阵列 , 电流500mA **, ** 5V步进 **
-
** A4988/TMC2209 **: ** 专用驱动 , 电流2A **, ** 细分 **
** 代码(四相八拍) **:
c
复制
uint8_t phase_seq[8] = {0x01, 0x03, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x09};
void step_motor(int steps) {
static uint8_t idx = 0;
for(int i=0; i<abs(steps); i++) {
if(steps > 0) idx++; // 正转
else idx--; // 反转
idx %= 8;
GPIO_Write(GPIOA, phase_seq[idx]); // 输出到PA0-PA3
delay_ms(2); // 速度控制
}
}
** 速度 **: ** delay_ms小 **→ ** 转速快 , 但 torque下降 , 需加减速曲线 **。
** 加减速 **: ** S曲线 **, ** 起步慢→加速→匀速→减速→停止 **。
** 76. 如何实现一个简单的PID控制器? ** ** PID=Proportional-Integral-Derivative **, ** 反馈控制经典算法 **。
** 公式**:U(t) = Kp×e(t) + Ki×∫e(t)dt + Kd×de(t)/dt
代码:
c
复制
typedef struct {
float Kp, Ki, Kd;
float setpoint;
float integral;
float last_error;
float output_limit;
} pid_t;
float pid_calc(pid_t *pid, float measured) {
float error = pid->setpoint - measured;
// P
float P = pid->Kp * error;
// I
pid->integral += error;
float I = pid->Ki * pid->integral;
// D
float D = pid->Kd * (error - pid->last_error);
pid->last_error = error;
// 输出
float output = P + I + D;
// 限幅
if(output > pid->output_limit) output = pid->output_limit;
if(output < -pid->output_limit) output = -pid->output_limit;
return output;
}
// 使用
pid_t motor_pid = {Kp: 1.0, Ki: 0.1, Kd: 0.01, setpoint: 1000, output_limit: 255};
while(1) {
float speed = read_encoder();
float pwm = pid_calc(&motor_pid, speed);
set_motor_pwm(pwm);
delay_ms(10);
}
** 调参 : ** Ziegler-Nichols法 , ** 先Kp振荡→ 再Ki消除静差**→**最后Kd抑制超调 **。
** 77. 在STM32上如何运行FreeRTOS? ** ** 步骤 **:
-
** 移植 **: ** source/portable/GCC/ARM_CM3 **, ** port.c **+ ** heap_4.c **
-
** 中断 **: ** SysTick中断 **→ ** xPortSysTickHandler() **
-
** 配置 : FreeRTOSConfig.h **
c
复制
#define configTICK_RATE_HZ 1000 #define configTOTAL_HEAP_SIZE (8 * 1024) -
创建任务:
c
复制
void task1(void *pv) { while(1) { GPIO_ToggleBits(GPIOA, GPIO_Pin_0); vTaskDelay(pdMS_TO_TICKS(500)); // 延时500ms } } int main(void) { SystemInit(); xTaskCreate(task1, "LED", 128, NULL, 1, NULL); vTaskStartScheduler(); // 启动调度器 }
** 启动 : vTaskStartScheduler() → ** 创建空闲任务→ ** 启动第一个任务 **。
78. 在FreeRTOS中,任务有哪几种状态? 4种状态:
-
** 就绪(Ready) : 万事俱备,只欠CPU , 等调度 **
-
** 运行(Running) : 正在执行 **
-
** 阻塞(Blocked) : 等事件**,**挂起 **, ** 不占CPU **
-
** 暂停(Suspended) : vTaskSuspend(), 人工暂停 , 不调度 **
** 转换 **:
复制
Running → Blocked: vTaskDelay()/xSemaphoreTake()
Blocked → Ready: 超时 or 事件到
Running → Ready: 时间片到 or 高优先级就绪
Ready → Running: 调度器选择
Running → Suspended: vTaskSuspend()
Suspended → Ready: vTaskResume()
**79. 什么是信号量、互斥量和消息队列? **
-
** 信号量(Semaphore) **= ** 资源计数 , N个资源 , 拿-1,放+1 **
c
复制
SemaphoreHandle_t sem = xSemaphoreCreateCounting(3, 3); // 3个资源 xSemaphoreTake(sem, portMAX_DELAY); // 拿 xSemaphoreGive(sem); // 放 -
** 互斥量(Mutex) **= ** 二值信号量+优先级继承 , 保护临界区 **
c
复制
SemaphoreHandle_t mutex = xSemaphoreCreateMutex(); xSemaphoreTake(mutex, portMAX_DELAY); shared_var++; xSemaphoreGive(mutex); -
** 消息队列(Queue) **= ** 数据缓冲 , 先进先出 **
c
复制
QueueHandle_t queue = xQueueCreate(10, sizeof(uint32_t)); xQueueSend(queue, &data, 100); xQueueReceive(queue, &data, portMAX_DELAY);
区别 :信号量计数 ,互斥量防优先级反转 ,队列传数据。
80. 如何避免任务间的优先级反转? ** 优先级反转 **: ** 低优先任务持锁 , 高优先级饿死 **。
** 解决 **:
-
互斥量 :FreeRTOS自动优先级继承 ,低优先级临时升
-
优先级天花板 :锁的优先级=最高可能拿锁的任务
-
无锁设计 :消息队列代替共享变量 ->同步用队列
-
关中断 :临界区极短 ,
<5行代码
最佳 :用互斥量 ,简单有效。
81. 什么是内存堆碎片?如何应对? 堆碎片=malloc/free后,内存千疮百孔 ,无法分配连续大块。
**应对 **:
-
** 不用malloc **: ** 静态分配 **
-
** 内存池 : 固定大小**,**无碎片 **
c
复制
#define BLOCK_SIZE 64 uint8_t mempool[BLOCK_SIZE * 10]; -
** TLSF分配器 **: ** 嵌入式专用 , 碎片率低 **
-
** 定期整理 : 重启复位 **
-
** MPU保护 : 检测越界 **, ** HardFault **
** RTOS **: ** FreeRTOS的heap_4.c有碎片合并 **。
** 82. 如何使用STM32CubeMX生成初始化代码? ** ** 步骤 **:
-
** 新建项目 **: ** 选芯片型号 **
-
** 时钟树 **: ** HSE输入8M → PLL×9=72M **
-
** 外设 **: ** 点UART1(Asynchronous) , 自动引脚PA9/PA10 **
-
**参数 **: ** Baud=115200 , NVIC使能 **
-
** 项目设置 : Toolchain=MDK-ARM **, ** 包名 **
-
** 生成代码 : GENERATE CODE **
-
** Keil打开 **: ** 在main.c的USER CODE区写业务 **
** 优势 **: ** 10分钟搭框架 **, ** 不查手册 , 引脚自动检查冲突 **。
**83. STM32CubeMX中生成的HAL库和LL库有什么区别? **
表格
复制
| 特性 | HAL库 | LL库 |
|---|---|---|
| ** 抽象度 ** | ** 高 ** | ** 低 ( 寄存器级 **) |
| **函数名 ** | ** 长 **( HAL_UART_Transmit ) |
** 短 **( LL_USART_Transmit ) |
| ** 代码量 ** | ** 大 ** | ** 小 ** |
| **可读性 ** | ** 好 ** | ** 接近寄存器 ** |
| ** 性能 ** | ** 低 ( 检查+状态机 **) | ** 高 ** |
| ** 移植性 ** | ** 好 ** | ** 差 ** |
** 选型 **: ** 新手用HAL , 熟手用LL , 追求性能混合用 **。
** HAL代码量 **: ** 10KB+ **, ** LL只有几百B **。
84. HAL库中的轮询、中断和DMA三种模式有何不同?
表格
复制
| 模式 | CPU占用 | 实时性 | 吞吐 | 适用 |
|---|---|---|---|---|
| ** 轮询 ** | ** 100% ** | ** 差 ** | ** 低 ** | ** 简单测试 ** |
| ** 中断 ** | ** 中 ** | ** 好 ** | ** 中 ** | ** 实时响应 ** |
| ** DMA ** | ** <5% ** | ** 极好 ** | ** 高 ** | ** 大数据流 ** |
** 轮询 :HAL_UART_Transmit(), while等标志 , CPU干等**。
** 中断 **:HAL_UART_Transmit_IT(), ** 发完进回调 , CPU干别的 **。
** DMA **:HAL_UART_Transmit_DMA(), **后台传 **, ** 完成回调 **。
** 推荐 **: ** 中断/DMA为主 , 轮询仅调试 **。
85. 如何处理HAL库中的超时错误? HAL_TIMEOUT =** 轮询超时间 **,HAL_MAX_DELAY=0xFFFFFFFF。
** 处理 **:
-
** 检查返回值 **:
c
复制
if(HAL_UART_Transmit(&huart1, data, len, 1000) != HAL_OK) { // 超时或错误 Error_Handler(); } -
** 调超时值 : 根据波特率算 , 100字节@115200≈10ms **
-
** 中断/DMA **: ** 不用超时 **, ** 异步通知 **
-
** 错误回调 **: ** 注册
HAL_UART_ErrorCallback()**
** 底层**:HAL_GetTick()计时,** SysTick中断++uwTick**。
** 86. 如何自定义HAL库的回调函数? ** ** 回调=中断服务后通知应用 **。
** 方式1:弱函数重定义 **:
c
复制
// stm32f1xx_hal_uart.c中有__weak void HAL_UART_RxCpltCallback()
// 在main.c中重新定义
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if(huart->Instance == USART1) {
// 处理
HAL_UART_Receive_IT(&huart1, rx_buf, 1); // 重新启动
}
}
** 方式2:注册回调 ( F4/H7支持 **):
c
复制
HAL_UART_RegisterCallback(&huart1, HAL_UART_RX_COMPLETE_CB_ID, my_rx_callback);
** 推荐 : 方式1简单 , 直接覆盖 **。
87. 如何通过SWD接口调试STM32? ** SWD接口=2根线**: ** SWDIO+SWCLK **。
** 连接 **:
复制
ST-Link → STM32
VCC → VDD
GND → GND
SWDIO → PA13
SWCLK → PA14
NRST → NRST(可选)
** Keil配置 **: ** Debug→ST-Link , SW模式 **, ** Flash Download勾选Reset and Run **。
** OpenOCD **:
bash
复制
openocd -f interface/stlink.cfg -f target/stm32f1x.cfg
gdb-multiarch -ex "target remote localhost:3333" firmware.elf
** 优势 **: ** 2线省引脚 **, ** 调试速度快 **, ** 可下载 **。
** 88. 断点有哪几种类型? **
-
** 软件断点 **: ** 修改Flash为BKPT指令 **, ** 无限个 **, ** 需改代码 **
-
硬件断点: ** 调试器硬件支持 **, ** 6个 **, ** 不改代码 **, ** 可在ROM设 **
-
** 数据断点(Watchpoint) : 变量读写断 , 4个 **
-
条件断点 :表达式满足才断 (i==100)
-
临时断点 :断一次自动删除
Keil用法 : **F9软件断点 , Debug→Breakpoints→Data Watchpoint **。
89. 如何通过串口打印调试信息? ** 串口printf=嵌入式"看门狗" **。
** 方案1:重定向fputc **
c
复制
int fputc(int ch, FILE *f) {
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, ch);
return ch;
}
// 直接用printf
**方案2:DMA后台发送 **:
c
复制
void debug_print(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
vsnprintf(tx_buf, TX_SIZE, fmt, args);
va_end(args);
HAL_UART_Transmit_DMA(&huart1, tx_buf, strlen(tx_buf));
}
** 注意 : ** ISR中禁止printf (慢 ),用环形队列。
**90. 如何使用ITM进行更高效的调试? ** ** ITM(Instrumentation Trace Macrocell) **= ** Cortex-M的"硬件printf" , 通过SWO引脚输出 **, ** 不占用UART **。
**配置 **:
-
** 使能Trace **:
c
复制
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; ITM->LAR = 0xC5ACCE55; // 解锁 ITM->TER = 0x1; // 使能端口0 -
** 输出函数 **:
c
复制
void ITM_SendChar(char ch) { while(ITM->PORT[0].u32 == 0); // 等FIFO空 ITM->PORT[0].u8 = ch; } -
** Keil调试 **: ** Debug→Settings→Trace→Enable **
-
** 查看 : Debug→printf Viewer **
** 优势**:** 速度=SWO时钟/10**,** <1μs延迟**,**ISR可用**。
**91. 如何测量代码的执行时间? ** ** 方法1:DWT周期计数器 ( Cortex-M3/M4/M7 **):
c
复制
// 初始化
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0; // 清零
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 使能
// 测量
uint32_t start = DWT->CYCCNT;
func(); // 被测函数
uint32_t cycles = DWT->CYCCNT - start;
float us = cycles / (SystemCoreClock / 1000000.0);
** 方法2:GPIO翻转+示波器 ( 最准 **):
c
复制
GPIO_SetBits(GPIOA, GPIO_Pin_0);
func();
GPIO_ResetBits(GPIOA, GPIO_Pin_0);
** 方法3:SysTick差值 **:
c
复制
uint32_t start = HAL_GetTick();
func();
uint32_t ms = HAL_GetTick() - start;
** 注意 : 优化级别影响 , -O0和-O3差10倍 **。
** 92. 如何配置STM32进入低功耗模式(睡眠、停止、待机)? ** **睡眠=CPU停,外设运行 **:
c
复制
__WFI(); // 等中断
**停止=PLL/HSE停 **:
c
复制
PWR_EnterSTOPMode(PWR_Regulator_ON, PWR_STOPEntry_WFI);
// 唤醒后需重新配时钟
SystemInit();
** 待机=只剩RTC **:
c
复制
PWR_EnterSTANDBYMode(); // 不返回,唤醒复位
** 配置停止模式 **:
c
复制
// 1. 使能PWR时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
// 2. 使能WKUP(PA0)
PWR_WakeUpPinCmd(ENABLE);
// 3. 进入
PWR_EnterSTOPMode(PWR_Regulator_ON, PWR_STOPEntry_WFI); // WFI等中断
// 或PWR_STOPEntry_WFE(事件)
** 功耗 **: ** Run=50mA , ** Sleep=5mA , Stop=50μA,**Standby=2μA **。
**93. 在低功耗模式下,哪些外设可以继续工作? **
表格
复制
| 模式 | 运行外设 | 唤醒源 |
|---|---|---|
| ** 睡眠 ** | ** 所有外设 ** | ** 任何中断 ** |
| ** 停止 ** | ** RTC ** 、** IWDG ** | ** EXTI 、 RTC闹钟 ** |
| ** 待机 ** | ** RTC ** 、 ** BKP ** | ** WKUP引脚 **、 ** RTC闹钟 ** |
** 注意 : 停止模式下,GPIO状态保持 , 复用时钟停 **。
**94. 如何分析和优化STM32的功耗? ** ** 分析 **:
-
** 万用表 : 测电源电流 **
-
** STM32CubeMonitor-Power **: ** PC软件 , 实时监控 **
-
** 示波器 **: ** 电流探头 , 看瞬时功耗 **
** 优化 **:
-
** 降频 **: ** 72MHz→24MHz , 功耗降50% **
-
** 关外设 **: ** RCC_APBxPeriphClockCmd **, ** 不用就关 **
-
** GPIO配置 **: ** 输入上拉/下拉 , 不浮空 , 输出低 **
-
**低功耗模式 **: ** Sleep/Stop **
-
** 外设时钟 **: ** LSI/LSE比HSE省 **
** 实测 **: ** F103 STOP模式 , 功耗从50mA→50μA **。
** 95. 在电路设计中,STM32的复位电路和晶振电路需要注意什么? ** ** 复位电路 **:
-
RC延时 :**R=10kΩ,C=100nF , 上电延时>1ms **(NRST低电平复位)
-
按键 :并联10nF电容 ,消抖
-
STLink :NRST接SWD ,调试器可复位
** 晶振电路 **:
-
**HSE : 8MHz **, ** 负载电容12pF±10% , 靠近OSC_IN/OSC_OUT , 走线短 **
-
**LSE : 32768Hz **, ** 6pF电容 , 配二级管防静电 **
-
** 匹配 : 晶振CL=负载电容= (C1×C2)/(C1+C2) + PCB杂散 **
** 反面教材 **: ** 晶振离芯片10cm → 不起振 , 电容错=频偏 **。
**96. 什么是去耦电容?为什么要尽可能靠近芯片电源引脚放置? ** ** 去耦电容=电源滤波+电荷储备 **, ** 滤高频噪声 **。
** 原理**:** 电源线有电感**,** 负载突变时电压跌落**,**电容瞬时供电**。
** 配置 **: ** VDD引脚 **→ ** 100nF陶瓷电容+1μF钽电容 **, ** 距离<5mm **。
** 布局**:** 每对VDD/VSS放1个100nF**,** 晶振底下不放**,**铺地**。
** 实测 : 不去耦 , GPIO翻转时电源纹波200mV , 去耦后<20mV **。
**97. PCB布局布线时,模拟部分和数字部分应如何处理? ** ** 分区隔离 **, ** 单点接地 **。
** 布局 **:
-
** 分区 : ADC/DAC/运放放一区 , MCU数字放一区 **
-
** 隔离 **: ** 0Ω电阻或磁珠 , 数字地≠模拟地 **
-
** 星型接地 : 单点汇到电源地 , 防地弹 **
-
** 去耦 : 模拟电源独立LDO , LC滤波 **
-
** 走线 : 模拟线短而粗 , 屏蔽 **, ** 远离数字线 **
** 反面 : 数字信号穿过模拟区 → ADC跳码 **。
** 98. 程序跑飞了,可能的原因有哪些? ** ** 跑飞=PC指向无效地址 **。
** 原因 **:
-
**栈溢出 **: ** 函数嵌套太深 , 数组越界 **
-
** 野指针 : 未初始化 , 释放后使用 **
-
** 中断向量 **: ** 未定义ISR , 进HardFault **
-
** 电源 : 电压不稳 , 复位不良 **
-
** 晶振 : 不起振 , PLL失锁 **
-
** 看门狗 : 没喂狗 , 频繁复位 **
-
** EMI : 干扰 , 引脚误触发 **
-
** 锁 : 读保护 **, ** IWDG锁 **
** 调试 : ** HardFault_Handler 中 ** 读LR→ 栈回溯**→**定位PC **。
** 99. 如何利用看门狗来增强系统的可靠性? ** ** 策略 **:
-
** 独立看门狗 : 防死循环 **
c
复制
IWDG_Enable(); IWDG_ReloadCounter(); // 主循环喂 -
** 窗口看门狗 : 防跑飞代码喂狗 **
c
复制
WWDG_Enable(0x7F); // 在窗口期喂 if(HAL_GetTick() - last_tick < 50) { WWDG_SetCounter(0x7F); } -
** 任务看门狗 **: ** 每个任务喂独立Flag , 守护进程检查 **
-
** 应用喂狗 : 关键操作后喂 , 不是定时器中断喂 **
** 最佳实践 : 复杂逻辑后喂 , 耗时操作分段喂 , 喂狗点=健康检查点 **。
**100. 在项目开发中,如何进行STM32的固件升级? ** ** OTA流程 **:
-
** Bootloader分区 **: ** 0x08000000(16KB) **
-
** APP分区 **: ** 0x08004000(剩余空间) **
-
** 升级流程 **:
-
APP通过** USART/WiFi 下载新固件→ 存0x08008000 ( 备份区 **)
-
** 校验CRC **
-
** 写Flash标志
new_fw_available**→**复位** -
Bootloader看标志→**搬0x08008000→0x08004000 → 跳APP **
-
** 安全措施 **:
-
** 双备份 **: ** A/B分区 , 升级失败回滚 **
-
** 签名验证 : RSA256 , 防篡改 **
-
** 断电保护 **: ** 写元数据双备份 , 原子操作 **
** 工具**:** IAP通过YModem协议**,**SecureCRT发送**。
总结 :STM32是"** 乐高积木 ", 搭好框架 , 填满细节 , 调试优化 , 产品落地**。这100问是"** 武功秘籍 ", 勤练则成 **。