资讯动态

STM32内部温度传感器与DMA实战:从原理到工程配置

发布时间:2026/10/14 1:49:24 来源:尧图企业网站定制
1. 为什么我最终选了内部温度传感器加DMA这条路线刚接触STM32那会儿测温度的第一反应就是外挂一个DS18B20或者LM75接线、写时序、读寄存器一套下来少说半天。后来项目里板子越做越小PCB上多一个元件都嫌挤我才认真回头看芯片手册里那个一直被忽略的“内部温度传感器”。它就在芯片内部连着ADC的一个固定通道不需要任何外围器件上电就能读对于做板级温度趋势监控、芯片过热保护这类需求简直是白送的资源。但真正让我决定把这套方案固化下来的是DMA。早期我用轮询方式读ADC主循环里塞一个HAL_ADC_GetValue读一次温度要等转换完成CPU干等着。后来改成定时器触发加中断每转换一次进一次中断频率一高CPU全耗在进出中断上了。直到把DMA拉进来ADC转换完自动把数据搬到内存数组CPU全程不参与我该跑控制算法跑控制算法该刷屏幕刷屏幕温度数据在后台默默就攒好了。这套组合——内部温度传感器 ADC DMA——成了我后来做电源管理、电机驱动、工业采集类项目的标配起手式。这篇文章我打算把这条链路彻底讲透。从内部温度传感器到底准不准、怎么换算到ADC的采样时间为什么要设那么长再到DMA的循环模式怎么配、双缓冲怎么玩最后把我踩过的坑一个个摆出来。不管你是刚点亮第一颗LED的新手还是已经用过ADC但没碰过DMA的老手应该都能从里面找到能直接抄作业的东西。代码我以STM32F1和F4这两个最经典的系列为主线HAL库和寄存器两个视角都会给因为实际项目里你不可能只靠HAL库走天下。2. 内部温度传感器到底是个什么东西2.1 它藏在芯片的哪个角落内部温度传感器在STM32里不是一个独立外设它本质上是ADC的一个输入通道。以STM32F103为例它挂在ADC1的通道16上STM32F407则挂在ADC1的通道16有些型号是通道18具体看手册。你不需要配置GPIO因为它没有引脚信号直接在芯片内部连到ADC的模拟输入端。这一点很关键——很多人第一次用的时候到处找“温度传感器引脚”其实根本没有。它的工作原理是利用半导体PN结的电压随温度变化的特性。芯片内部有一个带隙基准源产生一个与绝对温度成正比的电压PTAT电压这个电压经过内部缓冲后送到ADC。所以你读到的其实是一个电压值需要经过公式换算才能得到摄氏度。这个电压和温度的关系在芯片出厂时经过校准校准值存在系统存储区里这也是为什么它能做到不用外部校准就有一定精度的原因。2.2 精度这件事得说清楚官方手册里给的内部温度传感器精度典型值是±1.5℃全温度范围最大误差可能到±3℃甚至更多。这个精度什么概念测室温你只能看个大概测芯片结温做保护绰绰有余但你要拿它当温度计做产品那肯定不够。我实测过一批F103的片子同一块板子同一个环境不同芯片之间读数能差2℃以上同一颗芯片反复读跳动大概在0.5℃以内。所以我的经验是内部温度传感器适合做相对变化监测和过温保护不适合做绝对温度测量。比如你设定芯片超过85℃就降频或者关断这个场景它完全胜任因为误差3℃在保护阈值上留够余量就行。但你要显示“当前室温25.3℃”那还是老老实实上外部传感器。2.3 换算公式和校准值的来龙去脉F1系列的换算公式在参考手册里写得很清楚温度(℃) (V25 - Vsense) / Avg_Slope 25其中V25是25℃时的传感器输出电压典型值1.43VAvg_Slope是温度系数典型值4.3mV/℃。但这两个值是典型值每颗芯片实际值有偏差。好在出厂时ST把每颗芯片的校准值写进了系统存储区F1在地址0x1FFFF7B8和0x1FFFF7C2F4在0x1FFF7A2C和0x1FFF7A2E。读出来是两个16位值V25是12位ADC读数Slope是16位有符号数单位是mV/℃乘以1000。我强烈建议你用校准值而不是典型值。我对比过用典型值算出来和用校准值算出来同一颗芯片能差1℃以上。校准值读取的代码很简单// STM32F103 读取出厂校准值 #define V25_ADDR ((uint16_t*)0x1FFFF7B8) #define SLOPE_ADDR ((uint16_t*)0x1FFFF7C2) uint16_t v25_cal *V25_ADDR; // 25℃时的ADC读数 int16_t slope_cal *(int16_t*)SLOPE_ADDR; // 斜率单位mV/℃ * 1000注意F4的地址和F1不一样而且F4的V25是16位值但实际只有高12位有效读出来要右移4位。这些细节手册里都有但很容易看漏。3. ADC配置里那些不能随便填的参数3.1 采样时间为什么必须拉长内部温度传感器的输出阻抗比较高手册里明确要求最小采样时间。F1系列要求采样时间至少17.1微秒F4系列要求至少10微秒。这个时间不是随便定的因为ADC内部有一个采样保持电容信号源阻抗大的话电容充电到稳定值需要更长时间。如果你采样时间设短了电容没充到位读出来的值会偏低而且跳动很大。我刚开始用的时候图快把采样时间设成1.5个ADC周期结果读出来的温度比实际低了十几度还以为是芯片坏了。后来把采样时间设成239.5个周期F1的最大值读数立刻就稳了。F1的ADC时钟一般是14MHz72MHz六分频239.5个周期就是17.1微秒刚好满足手册要求。F4的ADC时钟可以到36MHz采样时间设成480个周期算下来13.3微秒也够了。这里有个计算过程你得会采样时间 采样周期数 / ADC时钟频率。比如F1的ADC时钟是14MHz你要17.1微秒那周期数至少是14M × 17.1u 239.4取整就是239.5。所以不是随便选个值就行得算。3.2 分辨率与对齐方式的选择ADC分辨率决定了温度读数的细腻程度。F1只有12位F4可以选12/10/8/6位。12位分辨率下参考电压3.3V最小分辨电压是3.3V / 4096 0.8mV。温度系数是4.3mV/℃所以理论上温度分辨率是0.8 / 4.3 ≈ 0.19℃。但实际因为噪声和传感器本身精度有效分辨率远达不到这个数。数据对齐方式分左对齐和右对齐。右对齐时读出来的值直接就是0到4095换算方便。左对齐时值在高12位低4位是0适合做8位或10位快速读取。我一般用右对齐因为换算公式直接套。3.3 扫描模式与连续转换的取舍如果你只读内部温度传感器一个通道那扫描模式关掉就行单通道单次转换。但实际项目里我经常要同时读多个通道比如电池电压、外部NTC、内部温度这时候扫描模式就派上用场了。扫描模式下ADC按顺序转换多个通道DMA把每个通道的结果依次搬到数组里。连续转换模式要慎用。它让ADC转换完一次立刻开始下一次不需要触发。如果你用DMA循环模式配合连续转换那数据会源源不断写入内存CPU完全不用管。但这样功耗会高一些而且如果你不需要那么高的采样率纯属浪费。我一般用定时器触发按需采样比如每100毫秒触发一次这样功耗和实时性都兼顾。4. DMA传输的配置细节与循环模式4.1 DMA请求是怎么产生的ADC转换完成后会产生一个DMA请求这个请求信号送到DMA控制器DMA控制器就把ADC数据寄存器的值搬到你指定的内存地址。整个过程不需要CPU干预。关键点在于ADC的DMA请求是在转换完成时产生的所以DMA传输的数据就是刚刚转换完的那个通道的值。在F1上ADC1对应DMA1的通道1F4上ADC1对应DMA2的流0通道0。这些对应关系在参考手册的DMA请求映射表里配错了DMA不会报错但数据就是不动这个坑我踩过。4.2 循环模式与普通模式的区别普通模式下DMA传输完指定数量的数据后就停止需要重新使能才能继续。循环模式下传输完指定数量后自动回到起始地址继续传输形成一个环形缓冲区。对于持续采集温度这种场景循环模式是首选因为你不希望采着采着DMA停了。但循环模式有个问题你怎么知道当前数据写到哪了如果你在主循环里读数组可能读到正在被DMA写入的那个元素数据可能只写了一半。解决办法是用双缓冲模式或者用DMA传输完成中断在中断里处理数据。我一般用传输完成中断在中断里把数据拷走或者置个标志位主循环根据标志位处理。4.3 双缓冲模式的高级玩法F4系列的DMA支持双缓冲模式有两个内存区域DMA写一个的时候你可以读另一个写完了自动切换。这个模式对于高速连续采集特别有用完全避免了读写冲突。配置的时候需要指定两个内存地址和各自的大小DMA会在两个缓冲区之间自动切换每次切换产生一个中断。我做过一个电机温度监测的项目用双缓冲模式一个缓冲区在DMA写入的时候另一个缓冲区被CPU读取并做滤波和过温判断切换中断里只需要交换一下指针。这样即使采样率很高也不会丢数据。5. 从零搭建的完整实操流程5.1 硬件准备与时钟配置硬件上你只需要一块STM32开发板什么外设都不用接。但要注意内部温度传感器和ADC的参考电压是共用VDDA的所以VDDA的稳定性直接影响温度读数。如果板子上VDDA和VDD之间没有磁珠或电感隔离数字部分的噪声会串进来读数会跳。我一般会在VDDA引脚旁边放一个1微法和一个10纳法的电容效果很明显。时钟配置方面ADC的时钟不能超过手册规定的最大值。F1的ADC时钟最大14MHzF4最大36MHz。如果你用72MHz主频F1的ADC预分频器设成6分频得到12MHz在范围内。F4的ADC时钟来自APB2一般是84MHz需要分频到36MHz以下我一般设成4分频得到21MHz或者2分频得到42MHz就超了得注意。5.2 初始化代码逐行拆解以F1的HAL库为例先看ADC初始化ADC_HandleTypeDef hadc1; DMA_HandleTypeDef hdma_adc1; void ADC_Init(void) { // 1. 使能时钟 __HAL_RCC_ADC1_CLK_ENABLE(); __HAL_RCC_DMA1_CLK_ENABLE(); // 2. DMA配置 hdma_adc1.Instance DMA1_Channel1; 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); // 3. 关联DMA到ADC __HAL_LINKDMA(hadc1, DMA_Handle, hdma_adc1); // 4. ADC配置 hadc1.Instance ADC1; hadc1.Init.ScanConvMode ADC_SCAN_DISABLE; // 单通道 hadc1.Init.ContinuousConvMode DISABLE; // 不连续转换 hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; // 软件触发 hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion 1; HAL_ADC_Init(hadc1); // 5. 配置通道 ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel ADC_CHANNEL_TEMPSENSOR; // 内部温度传感器通道 sConfig.Rank 1; sConfig.SamplingTime ADC_SAMPLETIME_239CYCLES_5; // 最长采样时间 HAL_ADC_ConfigChannel(hadc1, sConfig); // 6. 校准ADC HAL_ADCEx_Calibration_Start(hadc1); }这段代码里有几个点值得展开说。DMA_PERIPH_TO_MEMORY方向不能错ADC是外设数据从外设搬到内存。MemInc必须使能因为你要往数组里连续写多个数据内存地址要自增。PeriphInc要关掉因为ADC数据寄存器地址固定。数据宽度选半字16位因为ADC是12位用16位刚好。校准这一步很多人会漏掉。F1的ADC有校准功能不校准的话读数偏差可能到几十个LSB。校准必须在ADC使能之前做调用HAL_ADCEx_Calibration_Start就行内部会自动完成校准序列。5.3 启动采集与数据读取初始化完成后启动DMA传输#define TEMP_BUF_SIZE 32 uint16_t temp_dma_buf[TEMP_BUF_SIZE]; void Start_Temp_Acquisition(void) { HAL_ADC_Start_DMA(hadc1, (uint32_t*)temp_dma_buf, TEMP_BUF_SIZE); }这一句就把ADC和DMA都启动了。DMA会循环往temp_dma_buf里写数据写满32个后自动回到开头继续写。你可以在主循环里随时读这个数组但要注意读写冲突。我的做法是每读一次就关一下DMA中断或者用双缓冲。温度换算函数float Get_Temperature(uint16_t adc_value) { // 读取出厂校准值 uint16_t v25 *((uint16_t*)0x1FFFF7B8); int16_t slope *((int16_t*)0x1FFFF7C2); // 计算电压单位mV float voltage (float)adc_value * 3300.0f / 4096.0f; float v25_mv (float)v25 * 3300.0f / 4096.0f; // 换算温度 float temperature (v25_mv - voltage) / (slope / 1000.0f) 25.0f; return temperature; }注意slope的单位是mV/℃乘以1000所以要先除以1000。这个公式我用了很多年实测和外部传感器对比常温下误差在1℃以内。6. 那些年我踩过的坑和排查实录6.1 读数一直是0或者4095这是最常见的问题。读数一直是0说明ADC根本没转换或者DMA没搬数据。排查顺序先看ADC时钟使能没有再看DMA时钟使能没有然后看ADC的DMA请求使能没有。HAL库的HAL_ADC_Start_DMA内部会做这些但如果你自己写寄存器版本很容易漏。读数一直是4095说明输入电压超过了参考电压。内部温度传感器输出不会超过V25一般1.4V左右对应ADC读数大概1700多。如果你读到4095可能是通道选错了选成了某个接地的通道或者参考电压没接好。6.2 温度跳动特别大跳动超过2℃就不正常了。原因通常有三个采样时间太短、VDDA噪声大、DMA读写冲突。采样时间短是最常见的把采样周期数拉到最大基本能解决。VDDA噪声的话在VDDA引脚加电容或者软件上做滑动平均滤波。读写冲突的话用双缓冲或者读之前关DMA。我做过一个对比实验采样时间1.5周期时读数跳动范围是±15个LSB采样时间239.5周期时跳动范围降到±3个LSB。换算成温度前者跳动接近3℃后者不到0.6℃。所以采样时间这个参数千万别省。6.3 DMA传输一半就停了普通模式下DMA传输完指定数量就停这是正常的。如果你希望它一直传必须设成循环模式。还有一种情况是DMA传输完成中断里没有重新使能如果你用的是普通模式加中断中断里要再次调用启动函数。另外注意DMA的优先级如果系统里还有其他DMA通道在跑优先级低的可能被抢占导致数据丢失。我一般把ADC的DMA优先级设成高或者最高。6.4 温度换算出来是负数或者几百摄氏度这通常是校准值读取错误。F1和F4的校准地址不一样F4的V25值需要右移4位。如果你用F1的地址去读F4的芯片读出来就是乱码。还有slope是有符号数要用int16_t去读用uint16_t读出来会变成很大的正数算出来温度就是负的。我整理了一个速查表现象可能原因排查方法读数恒为0ADC或DMA未启动检查时钟使能、启动函数返回值读数恒为4095通道选错或参考电压异常确认通道号、测量VDDA电压跳动超过2℃采样时间短、噪声大加大采样周期、加滤波电容换算温度异常校准值地址或类型错误核对手册地址、检查数据类型DMA传输停止非循环模式或中断未重载改为循环模式、中断中重启6.5 低功耗模式下的注意事项如果你在做低功耗项目进入停止模式前要先把ADC和DMA关掉否则功耗降不下来。唤醒后重新初始化。另外内部温度传感器在低功耗模式下是不工作的因为ADC都停了。如果你需要低功耗下监测温度得用外部传感器加比较器的方式那是另一套方案了。7. 进阶玩法多通道扫描与定时器触发7.1 把内部温度和外部通道一起采实际项目里我经常要同时监测芯片温度和一路外部电压。这时候把ADC设成扫描模式通道序列里放内部温度传感器和外部通道DMA缓冲区大小设成通道数的整数倍。比如两个通道缓冲区设32那前16个是温度后16个是电压交替排列。配置的时候注意每个通道的采样时间可以单独设。内部温度传感器需要长采样时间外部电压如果源阻抗低可以用短采样时间。这样整体转换时间不会太长。7.2 定时器触发实现精确采样率软件触发虽然简单但采样率不精确受主循环影响。用定时器触发可以做到精确的采样周期。比如TIM2每100毫秒产生一次更新事件触发ADC转换。配置的时候把ADC的外部触发源设成TIM2的TRGO事件然后启动定时器就行。这样DMA缓冲区里的数据就是严格每100毫秒一个点做趋势分析或者FFT都很方便。我做过一个电源纹波监测的项目用定时器触发加DMA采样率做到10kHz连续采了10万点数据完整没有丢点。7.3 数据滤波的几种实用方法原始ADC数据肯定有噪声直接换算温度跳动大。我常用的滤波方法有三种滑动平均、中值滤波、一阶低通。滑动平均最简单开一个数组存最近N次的值每次算平均。中值滤波对脉冲噪声效果好取最近N次的中位数。一阶低通适合变化缓慢的信号公式是y y a * (x - y)a越小越平滑但响应越慢。温度这种缓变量我一般用滑动平均加一阶低通。滑动平均窗口取8或者16一阶低通的a取0.1左右。这样出来的温度曲线很平滑又不至于响应太慢。8. 寄存器版本的核心代码与对比8.1 为什么还要看寄存器HAL库虽然方便但代码量大执行效率低。在一些对实时性要求高的场合或者资源紧张的芯片上直接操作寄存器更合适。而且看寄存器能让你真正理解外设是怎么工作的出了问题也知道去哪找。8.2 F1的寄存器配置精简版// 使能时钟 RCC-APB2ENR | RCC_APB2ENR_ADC1EN; RCC-AHBENR | RCC_AHBENR_DMA1EN; // DMA配置 DMA1_Channel1-CPAR (uint32_t)ADC1-DR; DMA1_Channel1-CMAR (uint32_t)temp_dma_buf; DMA1_Channel1-CNDTR TEMP_BUF_SIZE; DMA1_Channel1-CCR DMA_CCR_MINC | DMA_CCR_CIRC | DMA_CCR_PSIZE_0 | DMA_CCR_MSIZE_0 | DMA_CCR_PL_1; DMA1_Channel1-CCR | DMA_CCR_EN; // ADC配置 ADC1-CR2 | ADC_CR2_ADON; // 先开一次 for(volatile int i0;i1000;i); // 等待稳定 ADC1-CR2 | ADC_CR2_CAL; // 校准 while(ADC1-CR2 ADC_CR2_CAL); ADC1-CR2 | ADC_CR2_CONT; // 连续转换 ADC1-CR2 | ADC_CR2_DMA; // 使能DMA ADC1-SQR1 0; // 1个转换 ADC1-SQR3 16; // 通道16内部温度传感器 ADC1-SMPR1 ADC_SMPR1_SMP16; // 239.5周期 ADC1-CR2 | ADC_CR2_ADON; // 再次开启开始转换寄存器版本代码量少很多但每一步都要自己确保正确。比如校准前要先开ADON校准完要等CAL位清零这些HAL库都帮你做了寄存器版就得自己写。8.3 两种方式的取舍HAL库适合快速开发和移植代码可读性好但效率低、代码体积大。寄存器版效率高、体积小但可读性差、移植麻烦。我的习惯是项目初期用HAL库快速验证功能功能稳定后如果资源紧张或者性能不够再把关键部分改成寄存器版。没必要一上来就死磕寄存器那是浪费时间。9. 实际项目中的经验沉淀9.1 温度数据的长期稳定性我有个项目连续运行了三个月用内部温度传感器监测芯片温度。发现一个现象刚上电时读数偏低大概半小时后稳定。这是因为芯片自身发热导致结温升高最终稳定在比环境温度高10到15℃的位置。所以如果你要测环境温度得等芯片热平衡或者做温度补偿。另外长期运行后读数会有轻微漂移大概每个月0.5℃的量级。这个漂移是传感器老化引起的无法避免。对于过温保护来说无所谓因为阈值留了足够余量。9.2 过温保护阈值的设定用内部温度传感器做过温保护阈值怎么定我的经验是先测出正常工作时的最高结温然后加20℃作为保护阈值。比如正常工作结温最高60℃那保护阈值设80℃。这样即使传感器有3℃误差也不会误触发。触发后不要立刻关断先降频或者降负载如果温度继续上升到第二阈值再关断。两级保护更稳妥。9.3 代码复用的封装建议这套ADC加DMA的代码我在多个项目里复用后来封装成了一个独立的模块。对外只暴露三个函数初始化、启动、获取温度。内部把校准值读取、DMA配置、换算公式都藏起来。这样换芯片型号的时候只需要改初始化里的通道号和校准地址上层代码不用动。封装的时候注意把缓冲区大小做成可配置的不同项目对采样率要求不一样。还有滤波参数也做成可配置的方便调整。9.4 调试技巧用DMA传输计数判断状态DMA的CNDTR寄存器里是剩余传输数量你可以通过读这个寄存器判断DMA是否在正常工作。如果CNDTR一直不变说明DMA停了。如果CNDTR在变化说明DMA在搬数据。这个方法比看标志位直观。还有一个小技巧在DMA传输完成中断里翻转一个GPIO用示波器看波形就能知道实际的采样率是多少。这个方法我经常用来验证定时器触发是否准确。10. 常见问题快问快答问内部温度传感器需要外部参考电压吗答不需要它用的是芯片内部的参考电压但ADC的参考电压是VDDA所以VDDA的稳定性会影响读数。问DMA缓冲区设多大合适答看你的采样率和处理频率。一般设成2的幂次比如16、32、64。太小了容易溢出太大了占内存。我一般用32。问为什么我的温度读数比实际高很多答大概率是芯片自身发热。内部温度传感器测的是芯片结温不是环境温度。芯片工作时结温比环境高10到20℃很正常。问可以用DMA同时传ADC和串口吗答可以但要注意DMA通道不能冲突。F1的DMA1有7个通道ADC1用通道1串口1的TX用通道4RX用通道5不冲突。配置的时候分别初始化就行。问停止模式唤醒后ADC读数不对答停止模式会关闭ADC唤醒后需要重新初始化和校准。我一般把初始化封装成一个函数唤醒后重新调用一次。问采样时间设成最大会影响实时性吗答会。239.5个周期在14MHz时钟下是17微秒如果你要高速采样这个时间就太长了。但内部温度传感器本身响应就慢不需要高速采样所以影响不大。如果你同时采其他快速变化的信号可以给不同通道设不同的采样时间。这套内部温度传感器加DMA的方案我从F1用到F4从裸机用到RTOS稳定性一直很好。关键就是把采样时间给够、校准值读对、DMA模式设成循环。剩下的就是根据项目需求调整采样率和滤波参数。代码写多了你会发现最难的不是配置本身而是知道为什么要这么配置。希望这篇东西能帮你把“为什么”搞清楚下次再配ADC的时候不用翻手册也能心里有数。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑