ARTICLE DETAIL

资讯详情

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

STM32内部温度传感器ADC+DMA采样滤波与两点校准详解

STM32内部温度传感器ADC+DMA采样滤波与两点校准详解 简介面向STM32入门与进阶开发者的完整Keil工程资源演示如何利用STM32F103C8T6的ADC配合DMA方式读取芯片内部温度传感器并经串口1输出温度数据。工程源码清晰实现了从ADC通道配置、DMA数据传输到USART串口发送的完整链路适合学习DMA搬运机制、ADC多通道采样以及HAL库回调处理的读者参考。压缩包共196个文件约4.57MB包含核心的.c源文件和.h头文件、工程配置文件如uvprojx/uvopt、编译生成的hex/axf及map/lst等辅助文件还附带Keil调试配置文件结构完整可直接打开编译学习。已有4937人学习参考。通过该工程可掌握内部温度传感器电压-温度换算方法、DMA中断与串口DMA发送的配合方式并可直接在此基础上扩展外部ADC通道或传感器采集应用是一份实用的嵌入式外设驱动学习样板。1. 温度传感器挂在ADC上为什么还要把DMA卷进来变流器监控板上最容易被忽略的温度点往往不是功率器件而是电源芯片和MCU自己的热点区域。外挂NTC要占用一个引脚、一版布局空间精度也不见得比芯片内部传感器好到哪里去STM32内部温度传感器虽然标称精度只有正负2度但它零成本、不占IO做板级过温保护完全够用。真正让新手翻车的是数据搬运用ADC中断去读结果寄存器中断延迟和主循环里的其他任务搅在一起采回来的序列周期性抖动换算出来的温度曲线毛刺很多。DMA方式把“转换完成”到“数据进内存”这段路径彻底交给硬件完成CPU只在DMA中断里取现成的数组采样节奏稳定得多。这篇内容顺着这条链路走先理清内部温度传感器的电气特性再搭ADCDMA的最小代码最后做滤波和两点校准适合做监控、保护类和DMA入门的人。2. 内部温度传感器采样原理与DMA搬运机制2.1 温度传感器输出特性与两套换算公式STM32内部温度传感器基于PN结正向压降的温度特性输出是一个随温度近似线性变化的电压信号内部直接连接到ADC的某个通道。以F4系列为例该通道对应ADC1_IN16它不需要外部引脚也不需要额外上拉电阻其他系列如F1的通道号也是16但G0/L4系列的通道映射不一样换型号时先查数据手册里的“Temperature sensor”章节。换算第一套公式用手册典型值T (V25 - VSENSE) / Avg_Slope 25其中V25是25度时的典型输出电压Avg_Slope是平均斜率。F4系列典型值V25 0.76 V、Avg_Slope 2.5 mV/°CF1系列是1.43 V、4.3 mV/°C。VSENSE由ADC原始码换算得到VSENSE VREF × ADC_Value / 409512位。这套公式适合快速验证误差来源是芯片间的工艺分散性。第二套是两点校准利用出厂时烧写在系统存储器里的校准值做线性插值能明显改善精度放到第5章展开。这里要再说清楚一个容易误会的点内部温度传感器测的是芯片裸片温度junction temperature不是环境温度。芯片满载运行几分钟后读数会比环境高十几度这是正常现象。它的作用场景是板级过热保护、工况监测不能替代环境温度计。2.2 EOC事件触发DMA与dma continuous requests的作用ADC完成一次转换后EOC标志置位转换数据已经锁存到ADC_DR数据寄存器DMA请求就是在此时发出的。外设基地址固定指向ADC_DR内存基地址指向一个全局数组每次转换完成搬一个半字16位传输完成后外设地址不变、内存地址递增这就实现了连续采样、顺序入队CPU全程不参与。但这里有一个从F1迁移到F4容易踩的寄存器细节DMA连续请求位。F4的ADC_CR2里有一个DMAContinuousRequests位它控制每个EOC事件是否持续产生DMA请求。连续转换模式ContinuousConvMode ENABLE开启后如果不把这一位置1F4的ADC完成第一轮转换后DMA请求就断了你会在内存里看到一组固定数据停在原地把这一位置1ADC每完成一次转换就发一次DMA请求配合DMA_CIRCULAR循环模式数据会像流水一样持续灌进数组。F1系列没有这个位它的DMA请求由CR2的DMA位控制循环搬运完全靠DMA本身的Circular模式维持移植代码时不要照搬F4的DMA配置。adc四通道使用dma也是这套机制区别只是把ScanConvMode打开、在每个Rank里配置不同通道DMA缓冲区长度要等于通道数乘以转换次数。多通道或者高速采集时DMA搬运与CPU读取会撞上。常见做法是把缓冲区拆成前后两半开通DMA半传输中断和传输完成中断CPU在中断里只处理已经写满的那一半另一半继续被DMA写入这样读写互不踩踏。温度采集速度很慢单缓冲区加平均值足够但了解这个机制对排查异常还是很有用的。关于数据一致性连续写入数组CPU读数组如果刚好读到DMA正在写的那个元素读写都是16位一次访问是原子的最坏情况是读取值“新一点”或“旧一点”对于平均滤波来说没有实际影响。真正要命的是缓冲区是局部变量栈上地址超出了DMA寻址范围或者地址不对齐导致HardFault这一点后面代码里注意。2.3 采样周期的取舍传感器高源阻抗带来的限制ADC采样的本质是对内部采样电容充电。外部信号源阻抗越高充电时间常数越大采样时间短了电容来不及充满测出来的电压就偏低。内部温度传感器在等效电路里是一个高输出阻抗的信号源所以芯片手册对它的采样时间有明确建议不是随便选个最小值就行。采样时间通过ADC_SMPR寄存器配置HAL库里对应SamplingTime字段。不同采样周期的实际表现ADC时钟约21MHzF4采样周期实际采样时长读数表现适用判断3 Cycles约0.14 µs读数偏低且逐次跳动大不适用内部传感器84 Cycles约4 µs读数稳定偶有±2 LSB波动常规板级监测够用480 Cycles约22.9 µs读数平稳受电源纹波影响小追求精度时选这个从84周期切到480周期多数板子上温度读数的峰峰值会从±4~6 LSB收窄到±1~2 LSB。原因有两个采样电容充电更充分以及更长采样窗口对高频噪声有一种近似平均的效果。温度信号本身变化很慢一次完整转换多花20微秒根本感知不到所以温度采集这里直接选最大采样周期最划算。adc采样周期太短带来的“读数普遍偏低”在温度通道上尤其明显因为内部传感器输出阻抗比外部引脚更高如果你发现常温下读到的是零下几十度先查这里而不是查公式。电源噪声对ADC的影响也不能忽略。VDDA引脚上的开关噪声会直接进入比较器参考电平表现在结果里是固定频率的随机跳动。PCB布局上常见的做法是VDDA到VREF之间串磁珠或RC滤波、模拟地与数字地单点连接、采样时钟走线远离开关节点。时钟抖动会降低ADC有效位数ENOB对12位分辨率来说通常不致命但如果你的板子同时布了电机驱动这类强干扰源温度读数每隔几百毫秒跳一次大值多半是地弹或参考电压被污染软件滤波只是掩盖问题。3. 用HAL库配置ADC1DMA并连续搬运原始码3.1 内部通道的引脚与时钟配置内部温度传感器通道不像普通ADC输入那样要初始化GPIO它在裸片内部连到ADC所以CubeMX里不需要为这个功能配置任何引脚。需要开放的开关是ADC通道使能时的温度传感器使能位TSVREFEHAL库在配置ADC_CHANNEL_TEMPSENSOR时自动处理这个位不需要手动碰寄存器。时钟配置的关键是ADC输入时钟不得超过芯片规定的上限F4是36MHz。ADC时钟来自APB2典型配置是PCLK2 84MHz经ADC预分频器4分频得到21MHz。CubeMX里对应ADC的Clock PrescalerHAL代码里是hadc1.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV4。如果这里倍率选小了ADC时钟超限转换时间参数会乱套最典型的症状就是EOC等待超时或DMA收到的数据整体发偏。另外ADC的DMA请求映射因芯片型号而异F4的ADC1对应DMA2 Stream0 Channel0F1系列对应DMA1 Channel1L4系列又是另一个映射关系同样需要先查数据手册的DMA request overview表格不要复用别的工程配置。3.2 ADC和DMA初始化代码与参数对照表下面是一份以STM32F4 HAL库为例的最小配置结构上分ADC和DMA两块。adc_raw必须是全局或static修饰的数组不要放在函数内部栈上。// 全局缓冲区DMA直接写入__IO 防止编译器缓存优化 static __IO uint16_t adc_raw[64]; // DMA2 Stream0 用于 ADC1 搬运 static void ADC1_DMA_Init(void) { __HAL_RCC_DMA2_CLK_ENABLE(); hdma_adc1.Instance DMA2_Stream0; hdma_adc1.Init.Channel DMA_CHANNEL_0; hdma_adc1.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_adc1.Init.PeriphInc DMA_PINC_DISABLE; // 外设地址固定 hdma_adc1.Init.MemInc DMA_MINC_ENABLE; // 内存地址递增 hdma_adc1.Init.PeriphDataAlignment DMA_PDATAALIGN_HALFWORD; hdma_adc1.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; hdma_adc1.Init.Mode DMA_CIRCULAR; // 循环模式 hdma_adc1.Init.Priority DMA_PRIORITY_HIGH; HAL_DMA_Init(hdma_adc1); __HAL_LINKDMA(hadc1, DMA_Handle, hdma_adc1); }DMA配置里PeriphDataAlignment和MemDataAlignment都用半字是因为ADC_DR是16位寄存器如果改成字对齐搬运结果会错位。方向是外设到内存PeriphInc固定关闭MemInc打开这样每次搬运后地址自动前进一个半字。Mode用DMA_CIRCULAR一轮64个点传完后自动从头开始不需要CPU重新发起请求这是DMA方式的精髓。接下来是ADC1本身的配置static void ADC1_Config(void) { __HAL_RCC_ADC1_CLK_ENABLE(); hadc1.Instance ADC1; hadc1.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode DISABLE; // 单通道 hadc1.Init.ContinuousConvMode ENABLE; // 连续转换 hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConvEdge ADC_EXTERNALTRIGCONVEDGE_NONE; // 关键F4上必须使能 DMA 连续请求否则只搬一次 hadc1.Init.DMAContinuousRequests ENABLE; hadc1.Init.EOCSelection ADC_EOC_SINGLE_CONV; HAL_ADC_Init(hadc1); ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel ADC_CHANNEL_TEMPSENSOR; sConfig.Rank 1; sConfig.SamplingTime ADC_SAMPLETIME_480CYCLES; // 最长采样周期 HAL_ADC_ConfigChannel(hadc1, sConfig); }主要adc参数对照表参数本配置取值为什么这样选Resolution12位温度链路不需要更高分辨率12位已够SamplingTime480 Cycles内部传感器高源阻抗需充分充电ContinuousConvModeENABLE让ADC自主连续转换不再依赖触发DMAContinuousRequestsENABLEF4关键位保证每个EOC都触发DMADirection外设到内存ADC_DR写到RAM数组ModeCIRCULAR搬运完自动重载无需重启DMA抄这段代码时最容易漏的一步是__HAL_LINKDMA它把DMA句柄挂到ADC句柄上。漏掉之后HAL_ADC_Start_DMA内部找不到DMA通道典型现象是函数执行后数组全零没有任何中断。另外HAL_ADC_Init在内部会调用HAL_ADC_MspInit如果DMA初始化放在MSP回调里要注意DMA时钟开启不能放在ADC初始化之后顺序错了会出现DMA未就绪的HardFault。3.3 启动顺序从校准到DMA开始搬运初始化完成后调用顺序如下// 1. 先做ADC自校准减小偏移误差F4支持 HAL_ADCEx_Calibration_Start(hadc1); // 2. 启动ADC并开始DMA搬运adc_raw会持续被刷新 HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_raw, 64);自校准要在连续转换启动之前做否则校准过程会干扰转换状态。F4的HAL_ADCEx_Calibration_Start内部会重新初始化ADC调用后原来的配置仍然有效F1系列没有这个函数需要用寄存器方式触发CAL位校准。HAL_ADC_Start_DMA的最后一个参数是转换个数不是字节数这里传64表示搬满64个半字后触发传输完成中断在循环模式下并不会真的停止而是清标志后自动重载。启动后不要立即去读adc_raw至少要等完成一次完整搬运否则数组里一半是旧值一半是新值。稳妥做法是打开DMA的传输完成中断在回调里置一个标志位主循环检测到标志位后再做平均与换算。注意adc_raw 必须声明成全局或 static 变量且用 __IO 修饰写成局部数组会让 DMA 写入栈内存轻则数据不动重则直接 HardFault。多通道温度加其他传感器一起采集时扫描模式对应ScanConvMode ENABLE每个通道配置Rank和采样时间DMA缓冲区长度改为通道数乘以每通道样本数。adc四通道使用dma的场景里缓冲区分组排列和数据分割是排查重点否则换算时会把通道数据错位。4. 从原始码到温度的换算滤波、公式与漂移排查4.1 均值加去极值滤波压住单次跳变温度信号带宽极低64次采样的时间也就1.5毫秒左右直接做均值滤波。但在均值之前要先去极值工业现场偶尔会出现电源干扰打进ADC产生一个偏离正常范围很大的坏点如果不剔除一个跳变点就能让平均结果整体偏移几十个数。去极值做法是找出最大值和最小值求和序列去掉这两个后取平均uint32_t sum 0; uint16_t vmin 0xFFFF, vmax 0; for (int i 0; i 64; i) { uint16_t v adc_raw[i]; sum v; if (v vmin) vmin v; if (v vmax) vmax v; } sum sum - vmin - vmax; // 去掉一个最大值和一个最小值 float lavg (float)sum / 62.0f; // 剩余62个点求平均这段代码在单循环内完成累加、最小值、最大值检测然后在累加和里扣除最值。注意vmin的初值必须用0xFFFF而不是0否则求最小值逻辑失效。平均值lavg是“干净”的原始码后面再参与温度换算。滤波策略对精度的影响可以量化处理方式输出波动峰峰值换算温度波动单次读取±6 LSB±0.8 °C64点均值±2 LSB±0.25 °C去极值64点均值±1 LSB±0.12 °C温度显示要平滑但也不要平滑过头。如果均值窗口拉到256点滤波延迟增加到约6毫秒其实无所谓但如果你同时想做快速过温保护就不能依赖长时间平均应该在DMA半传输中断里立刻算出平均值把响应时间压到半缓冲区时长以内。4.2 温度换算代码与两套公式的选择拿到滤波后的原始码先换算成VSENSE再套温度公式。用F4典型值做快速换算float adc_to_temp_quick(float lavg) { float vsense 3.3f * lavg / 4096.0f; // 参考电压3.3V12位分辨率 return (0.76f - vsense) / 0.0025f 25.0f; // V250.76V, 斜率2.5mV/°C }这里有一个隐含假设ADC参考电压等于3.3V。如果你的板子上VDDA被一个高精度基准源供电那要把3.3f替换成实际基准电压否则温度整体偏移。F1系列由于V25和Avg_Slope不同不能直接套这个函数。快速公式适合尝鲜但做产品级监测不建议因为手工测量得到的斜率分散性、以及电源电压误差都会让温度误差跑到±5°C以上。要更准就得用下一章的出厂校准两点插值。4.3 adc数据漂移的排查思路数据漂移通常分两类真漂移和伪漂移。真漂移是芯片温度本身在变比如负载电流变化导致裸片发热或开关电源散热传导到MCU区域这类漂移缓慢、规律性强往往和系统功耗状态有对应关系伪漂移是ADC参考电压或采样条件变化引起的读数变化芯片温度没动读出来却在跑。排查时先用内部参考电压通道VREFINT做对照。VREFINT是一个恒定的内部基准约1.2V如果VREFINT读数稳定而温度读数在漂说明问题在温度传感器链路如果VREFINT也跟着漂先从VDDA供电入手检查参考电压纹波和磁珠是否发热。另一个常见排查手法是改变采样周期看结果是否跟着变。同一个温度下从84周期切到480周期读数如果明显变化并趋于平缓说明之前的采样时间不足采样电容没充满低读数不是真实温度。PCB上给ADC用的3.3V模拟电源如果和数字电源共用一个LDO数字侧开关噪声会通过地平面传导进参考电压布局上把VDDA走线加粗、加0.1微法去耦电容并让采样时钟走线和开关节点拉开距离都是低成本且有效的处理方式。外置三点校准之类的做法更适合批量生产场景这里先不展开。5. 用出厂校准点做两点插值并验证测温链路5.1 读取TS_CAL1/TS_CAL2做两点校准快速公式误差大的根源在于V25和Avg_Slope都是“典型值”每个芯片实际值和它都有偏差。STM32出厂时会在系统存储器里烧写两个校准点TS_CAL1对应30°C时的ADC原始码TS_CAL2对应110°C时的ADC原始码。F407这两个值位于0x1FFF7A2C和0x1FFF7A2E各占16位其他系列的地址在参考手册的“Temperature sensor calibration values”一节里查表获得别跨系列照抄。uint16_t cal1 *(uint16_t *)0x1FFF7A2C; // 30°C校准原始码 uint16_t cal2 *(uint16_t *)0x1FFF7A2E; // 110°C校准原始码 float lavg get_filtered_avg(); // 上一节的去极值均值 float temp (float)(lavg - cal1) * (110.0f - 30.0f) / (float)(cal2 - cal1) 30.0f;两点插值的本质是用芯片自己的两个实测点做线性映射抵消了斜率分散性带来的大部分误差。实测在F407上校准后常温读数与外部NTC的差值在±1.5°C以内比快速公式的接近±4°C改善明显。如果对精度要求更高还可以在批量产线上做三点或更多温度点的个体校准把结果写进EEPROM或Flash特定区域运行时再读取。5.2 温度输出的验证方法与串口DMA发送技巧改完代码先别急着看温度数值。第一步在恒定室温下连续打印100次记录原始码峰峰值和温度最大最小值这一步验证滤波是否生效第二步用热风枪吹芯片表面观察读数上升速度与外部NTC是否同步响应方向必须一致第三步让系统满载运行30分钟画温度曲线确认没有周期性漂移。串口输出建议用串口DMA主循环里只需要构建字符串然后交给外设避免串口阻塞占用采样节奏也正好复用前面DMA的思路static char txbuf[32]; snprintf(txbuf, sizeof(txbuf), temp:%.2f\n, temp); HAL_UART_Transmit_DMA(huart1, (uint8_t *)txbuf, strlen(txbuf));txbuf用了static是不能省掉的串口DMA是异步的函数返回时数据可能还没发完如果缓冲区是栈上的局部变量出函数后内存被复用发出去的就是乱码。ADC的adc_raw数组用__IO修饰也是一样的道理告诉编译器这个变量会由硬件外设修改不要在寄存器里缓存旧值。这两个修饰符看起来不起眼却是DMA调试里最常见的隐性故障源也是从“能跑”到“稳定跑”之间最值得检查的两处。本文还有配套的精品资源点击获取
返回列表