STM32内部温度传感器+DMA采样:从CubeMX配置到滤波换算实践
2026/9/17 0:24:23 网站建设 项目流程

简介:STM32内部温度传感器结合ADC与DMA的实战工程包,面向嵌入式初学者和STM32开发者,演示从ADC通道配置、DMA数据传输到USART1串口输出的完整流程,适合需要掌握低CPU占用数据采集方法的场景。压缩包共196个文件,约4.57MB,包含C源码、头文件、STM32标准库工程文件、编译中间文件及hex文件,其中.c/.h文件对应ADC、DMA、USART等驱动实现,.uvprojx/.uvoptx为Keil工程配置,hex可直接烧录验证。已有4937人学习下载,配套工程在F103C8T6上实现内部温度传感器输出电压读取,并经DMA自动搬运数据,再由串口打印温度值,同时提供中断回调处理与转换完成事件管理,可帮助快速理解库函数下多外设协同设计思路。资源目录结构完整,适合对照源码逐行学习或直接移植到相近项目。

1. 用DMA搬内部温度数据,是温控类STM32项目最省心的起点

STM32 内部温度传感器经常被当成“ADC 入门玩具”,实际上它是验证 DMA 链路最合适的小信号源:不需要外接导线,不会因为接地环路引入额外噪声,而且采样值会和芯片负载、电源电压一起变化。用轮询方式读它当然可以,但系统里一旦还有显示、通信、按键,轮询要么让温度刷新变慢,要么打断其他时序。DMA 方式实现后,ADC 完成一次转换就自动把数据写进内存,CPU 只在一个完整转换周期后去消费最新温度,这时才谈得上“采样不占 CPU”。

这篇以 STM32F1 / F4 系列的 HAL 库为主线,把内部温度传感器接到 ADC1 专用通道,用 DMA 的连续请求让数据自动流动,然后做滤波、温度换算和串口输出。适合刚把HAL_ADC_Start_DMA跑通,想在温控、功耗监测里用内部温度做芯片结温预估的开发者。你会看到采样周期、DMA 连续请求、校准值读取和滤波之间如何互相影响。

2. 内部温度传感器采样原理:专用通道、参考电压和采样周期

2.1 内部温度传感器对应哪个 ADC 通道

内部温度传感器不是普通引脚输入,它和带隙参考电压一起挂在模拟开关后面。F1 系列对应ADC1_IN16,F2 / F4 系列对应ADC1_IN18,G0 系列可能是ADC1_IN9,具体要查参考手册里的“Temperature sensor”章节。这个通道只能由 ADC1 访问,ADC2 和 ADC3 读不到。

使用前必须先使能内部传感器开关:

__HAL_RCC_ADC1_CLK_ENABLE(); // 打开 ADC1 时钟 HAL_ADCEx_EnableVREFINT(); // 使能内部参考电压 HAL_ADCEx_EnableInternalSensor(); // 使能内部温度传感器

F1 系列 HAL 库里不一定有HAL_ADCEx_EnableInternalSensor,需要直接操作寄存器:

ADC1->CR2 |= ADC_CR2_TSVREFE; // 同时使能内部参考电压和温度传感器

这段代码要注意顺序:先打开 ADC1 时钟,再使能内部参考电压,最后使能温度传感器。反过来操作做时,部分系列第一次采样会读到 0 或旧通道残留值。内部温度传感器返回的仍是相对于VREF+的 ADC 码,不是绝对电压,所以VREF+不稳定时读数也会漂。

2.2 采样周期选多少才不是白采

内部温度传感器的输出阻抗比外部传感器高,参考手册通常要求采样时间至少 10µs。如果 ADC 时钟是 14MHz,10µs 对应约 140 个 ADC 时钟周期,那么 28.5 周期的采样时间根本不够,至少要选 71.5 或 239.5 周期。ADC 总转换时间大约是采样周期加上 12 位逐次逼近的 12 个周期,下表按 14MHz ADC 时钟计算:

ADC 时钟采样周期配置采样时间总转换时间(12 位)
14MHz1.5 周期0.107µs0.964µs
14MHz13.5 周期0.964µs1.821µs
14MHz28.5 周期2.036µs2.893µs
14MHz71.5 周期5.107µs5.964µs
14MHz239.5 周期17.107µs17.964µs

温度信号本身变化很慢,不需要追求 1Msps 的采样率。打开 DMA 连续请求后每秒采 50 次已经足够,所以推荐先选 239.5 周期。这个值同时会降低开关电容充放电不充分造成的 adc 数据漂移。

2.3 温度换算公式和出厂校准值

F2 / F4 / F7 系列在出厂时把 30°C 和 110°C 对应的 ADC 原始值烧录在系统存储区,直接读取:

uint16_t cal30 = *(uint16_t*)0x1FFF7A2C; // 30°C 时的 12 位 ADC 原始值 uint16_t cal110 = *(uint16_t*)0x1FFF7A2E; // 110°C 时的 12 位 ADC 原始值

换算公式是线性插值:

temp = (110 - 30) * (adc_raw - cal30) / (cal110 - cal30) + 30;

F1 系列没有这两个校准寄存器,只能使用数据手册典型值:V25 = 1.43VAvg_Slope = 4.3mV/°C。换算时要把 ADC 码转回实际电压,并且必须传入真实的VREF+电压:

float vsense = adc_raw * vref / 4095.0f; temp = (1.43f - vsense) / 0.0043f + 25.0f;
系列内部通道校准方式典型精度
F1ADC1_IN16无存储校准值,用 V25 和斜率约 ±3°C,VREF 不稳会更大
F4 / F2ADC1_IN180x1FFF7A2C / 0x1FFF7A2E约 ±2°C
G0 等查参考手册各型号不同需按手册确认

F4 系公式里先乘 80 再除,中间值最大约 327600,用int32_t就能装下,不需要浮点。读取校准寄存器时建议加volatile,防止编译器把两次指针解引用合并成一次优化。

2.4 DMA 连续请求控制的是“每个转换完成都搬”

CubeMX 里有一项DMA Continuous Requests,它和 ADC 的Continuous Conversion Mode是两个不同层面的开关。ADC 连续转换决定的是“转换结束后是否立刻开始下一次转换”,DMA 连续请求决定的是“每个转换结束是否都触发 DMA 搬运”。

关闭 DMA 连续请求时,即使 ADC 在连续转换,DMA 也可能只响应一次请求,然后停在旧值上。打开后,DMA 跟着每次 EOC 事件走,才能实现真正的自动刷新。这里容易踩的坑是把 DMA 模式设成Normal,数据搬完一轮就停;温度只有第一次正确,后面全是旧数组。用内部温度传感器时,建议设置 ADC 连续转换、DMA 连续请求Enable、DMA 模式Circular,这样启动一次就一直采样。

3. 用 CubeMX 配置 STM32 ADC 内部温度传感器和 DMA 的最小工程

3.1 CubeMX 里需要动的一串开关

在 CubeMX 的 Pinout 视图选择ADC1,在右侧勾选IN16IN18。注意是“ADC1”下的内部通道,不是ADC1_INP16这种外部输入。如果 Pinout 图上原本被占用的引脚自动变成了ADC1_IN16,把它点回Reset_State,因为内部温度传感器不占物理引脚。

接着按下面顺序操作:

  1. 将 ADC1 的Continuous Conversion Mode设为Enable
  2. DMA Continuous Requests设为Enable
  3. 在 DMA Settings 里添加ADC1,方向选Peripheral To Memory,模式选Circular
  4. Memory Increment选择Enable,目的是让 DMA 把连续转换结果按顺序写进数组。
  5. 回到 Parameter Settings,把Sampling Time选到 239.5 周期(F4 或 F1 中对应宏可能不同,单位都是 ADC 时钟周期)。

关键参数推荐值如下表:

配置项推荐值说明
Clock PrescalerPCLK2 / 4 或 / 6让 ADC 时钟落在器件允许范围内,F1 建议 ≤ 14MHz
Resolution12 位内部温度传感器不需要更高分辨率
Scan Conversion ModeF1 需EnableF1 的 HAL 驱动在扫描模式下才能选内部通道 rank
Continuous Conversion ModeEnable保证温度数据持续更新
DMA Continuous RequestsEnable每次转换完成后都触发 DMA 请求
DMA ModeCircularDMA 循环搬运,不需要反复手动启动

3.2 生成的初始化代码为什么长这样

CubeMX 会生成MX_ADC1_Init,核心结构如下:

static void MX_ADC1_Init(void) { hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE; // F1 单通道也必须开启扫描 hadc1.Init.ContinuousConvMode = ENABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 1; hadc1.Init.DMAContinuousRequests = ENABLE; hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV; HAL_ADC_Init(&hadc1); }

参数说明:ScanConvMode在 F1 系列上必须保持ADC_SCAN_ENABLE,否则内部温度传感器通道不会进入转换序列,读出来始终是 0。NbrOfConversion = 1表示只有一个 rank,变量名看起来像“通道数”,实际含义是“转换序列里的 rank 数量”。EOCSelection设为ADC_EOC_SINGLE_CONV,这样 DMA 请求在每次单次转换结束就触发,而不是等整个序列结束。

随后会有通道配置:

sConfig.Channel = ADC_CHANNEL_TEMPSENSOR; // 内部温度传感器通道 sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; // 采样周期 239.5 HAL_ADC_ConfigChannel(&hadc1, &sConfig);

这里的ADC_CHANNEL_TEMPSENSOR是库替你做好的宏,F1 展开后是ADC_CHANNEL_16,F4 展开后是ADC_CHANNEL_18。如果自己用寄存器写,要注意这个宏不是所有旧固件库都有。

3.3 启动 DMA 转换的标准调用顺序

main函数里,常见做法是:

int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_ADC1_Init(); MX_UART4_Init(); // 如果有串口输出 static uint16_t adc_buf[16] = {0}; // 必须全局或 static HAL_ADCEx_EnableVREFINT(); HAL_ADCEx_EnableInternalSensor(); HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, 16); while (1) { // 主循环只消费温度结果 } }

HAL_ADC_Start_DMA的参数解释:第一个是 ADC 句柄,第二个是 DMA 写入的目标地址,第三个是Data Length,单位是“半字”而不是字节。因为 12 位 ADC 结果放在 16 位半字里,所以长度16表示目标数组有 16 个元素,不是 32 字节。如果填成sizeof(adc_buf),DMA 会多写一倍数据,直接越界。

注意不要把adc_buf定义成main里的普通局部变量。DMA 在后台持续写这个地址,函数退出后栈空间会被复用,轻则数据被覆盖,重则硬件错误。static或者全局数组都行。

3.4 国产兼容芯片上常见的 DMA 数据紊乱

如果你用的是 GD32E230 或 APM32 这类兼容 STM32 引脚和库函数的芯片,可能会遇到“启动后温度值乱跳、通道错位”的现象。这不是 ADC 或 DMA 协议本身的问题,而是这些芯片的 DMA 请求映射在连续转换模式下和 ST 原厂不完全一致。

用 GD32E230 的常见规避方法是:把DMAContinuousRequests关掉,改用 ADC 转换结束中断,在中断回调里手动复制当前ADCx->DR,或者把 DMA 从Circular改成Normal后每次回调重新HAL_ADC_Start_DMA。这类芯片上优先保证功能正确,再考虑要不要省 CPU 中断。排查时先确认 DMA 中断标志位是否清除,以及NVIC里 ADC 中断优先级是否比 DMA 高。

4. 从 ADC 原始值到温度:DMA 回调、滤波和串口 DMA 输出

4.1 用 DMA 传输完成回调锁存数据

启动 DMA 后,温度数据在后台持续更新。主循环不能直接读adc_buf[0],否则可能读到一半的暂态值。更稳妥的做法是在 DMA 回调里做一次整体拷贝:

#define ADC_BUF_SIZE 8 __IO uint16_t adc_buf[ADC_BUF_SIZE]; uint16_t temp_raw_copy[ADC_BUF_SIZE]; volatile uint8_t adc_ready = 0; void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance != ADC1) return; for (int i = 0; i < ADC_BUF_SIZE; i++) { temp_raw_copy[i] = adc_buf[i]; } adc_ready = 1; }

__IO在 STM32 HAL 里就是volatile,提醒编译器每次都从内存读取,不要优化到寄存器缓存里。回调函数里做 8 次半字拷贝非常快,ADC 下一次转换完成前足够执行完。adc_ready置 1 后,主循环可以每隔一段时间消费一次,不需要每次都执行滤波。

这里要注意:如果 DMA 配置成Circular,回调函数会在每 8 次转换完成后触发一次,而不是每次转换都触发。若想要每次转换都有回调,把 DMA 目标数组长度设为 1,或者使用HAL_ADC_ConvHalfCpltCallback。温度信号是慢变量,8 点一组完全够用。

4.2 温度换算要区分 F1 和 F4

转换函数写成分装形式,便于以后移植其他系列:

/* F1 内部温度换算:使用 V25 和 Avg_Slope */ float convert_temp_f1(uint16_t raw, float vref) { float vsense = raw * vref / 4095.0f; return (1.43f - vsense) / 0.0043f + 25.0f; } /* F4 内部温度换算:使用出厂校准值 */ int16_t convert_temp_f4(uint16_t raw) { int16_t cal30 = *(volatile int16_t *)0x1FFF7A2C; int16_t cal110 = *(volatile int16_t *)0x1FFF7A2E; return (110 - 30) * (raw - cal30) / (cal110 - cal30) + 30; }

参数说明:F1 版本里的vref是实际的VREF+电压。如果直接用 3.3V 计算,而电源实际是 3.28V,温度值会产生大约 1°C 以上的偏差。F4 版本不关心VREF+具体是多少,因为出厂校准值也是 12 位 ADC 码,比值关系消掉了参考电压误差。

F4 版本返回值是“实际温度 × 10”:

int16_t temp = convert_temp_f4(adc_value); // temp = 312 表示 31.2°C printf 时用 temp / 10 和 temp % 10 拆整数和小数。

4.3 C 语言 ADC 值滤波函数:滑动平均和中值

内部温度传感器受电源噪声和 PCB 布局影响,单次读数跳几个 LSB 很正常。常用的 C 语言 ADC 值滤波函数有两种。

滑动平均:

#define FILTER_LEN 8 static uint16_t history_buf[FILTER_LEN]; static uint8_t history_idx = 0; static uint32_t history_sum = 0; uint16_t adc_sliding_filter(uint16_t new_sample) { history_sum -= history_buf[history_idx]; history_buf[history_idx] = new_sample; history_sum += new_sample; history_idx = (history_idx + 1) % FILTER_LEN; return (uint16_t)(history_sum / FILTER_LEN); }

滑动平均的思路是维护一个固定窗口的和,每来一个新采样,先减去最旧值,再加上新值,最后取平均。注意素组初始化时history_sum必须是 0,history_buf最好清零,否则前 8 次输出会被垃圾值污染。history_idx指向最旧的数据位置,用取模让它循环覆盖。

中值滤波:

#define MEDIAN_N 5 uint16_t adc_median_filter(uint16_t sample) { static uint16_t ring[MEDIAN_N]; static uint8_t idx = 0; ring[idx] = sample; idx = (idx + 1) % MEDIAN_N; uint16_t sort_buf[MEDIAN_N]; for (int i = 0; i < MEDIAN_N; i++) { sort_buf[i] = ring[i]; } for (int i = 0; i < MEDIAN_N - 1; i++) { for (int j = i + 1; j < MEDIAN_N; j++) { if (sort_buf[i] > sort_buf[j]) { uint16_t tmp = sort_buf[i]; sort_buf[i] = sort_buf[j]; sort_buf[j] = tmp; } } } return sort_buf[MEDIAN_N / 2]; }

中值滤波对尖峰毛刺抑制更强,但每次要复制和排序,CPU 开销高一些。温度变化慢,用 5 点中值足够。如果目标是降低白噪声,滑动平均效果更好。

滤波方式延迟毛刺抑制适用场景
滑动平均窗口一半长度一般采样率较高、噪声均匀
中值滤波取决于窗口排序电机启停、电磁干扰尖峰
先中值再平均更高温度值既要平滑又要抗尖峰

建议在 DMA 回调里只拷贝,在主循环或定时器里做滤波和换算。不要在 DMA 中断里做排序,否则会拉长中断时间,影响其他实时任务。

4.4 串口 DMA 输出温度值

温度算出来后,通常要发到串口监控。既然用了 DMA,发送端也应该用 DMA,避免主循环被阻塞:

char uart_tx_buf[64]; volatile uint8_t uart_tx_busy = 0; void send_temperature(int16_t temp) { int int_part = temp / 10; int frac_part = temp % 10; int len = snprintf(uart_tx_buf, sizeof(uart_tx_buf), "t=%d.%d C\r\n", int_part, frac_part); if (!uart_tx_busy) { uart_tx_busy = 1; HAL_UART_Transmit_DMA(&huart4, (uint8_t*)uart_tx_buf, len); } } void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == UART4) { uart_tx_busy = 0; } }

HAL_UART_Transmit_DMA是异步发送,第三个参数是字节数。uart_tx_busy防止上一次 DMA 还没发完,下一次snprintf就改写缓冲区,这是串口 DMA 最常出现的乱码原因。如果发送频率超过 DMA 传输速度,剩余数据会被丢弃;温度场景下每秒一次,不会有问题。

如果想彻底避免丢数据,可以把uart_tx_buf做成环形缓冲区,发送完成回调里从队列取下一帧再发。这里不展开,先保证单帧不覆盖。

5. 校验内部温度结果的 3 个实用技巧

5.1 用环境温度做单点偏移修正

出厂校准值已经比直接用典型值准确很多,但每块板子的电源、布局、外壳散热不同,仍可能差 1~2°C。F4 系列可以在常温下用温度计读取环境温度,和内部温度传感器输出比较,得到cal_offset

int16_t calibration_offset = measured_temp - reported_temp; int16_t corrected_temp = reported_temp + calibration_offset;

calibration_offset存在内部 Flash 或 EEPROM 模拟区,重上电后读取。注意环境温度计要贴近 MCU,不要放在电解电容旁边,否则修的是电容温度。

5.2 用调试器区分 DMA 错位和真实噪声

数值跳变不一定来自噪声,很可能是 DMA 配置问题。在调试器里暂停运行,直接查看adc_buf数组里各元素。如果这些数值在 5 个 LSB 内波动,说明是噪声;如果出现整段数值接近满量程 4095,或者从第 N 个元素开始全是 0,先检查 DMA 循环模式和DMAContinuousRequests

F1 系列上还要确认ScanConvMode是否打开。单通道扫描模式下,adc_buf[0]adc_buf[1]可能是两个不同 rank 的结果;当NbrOfConversion = 1时,多余的数组元素是上一次循环残留值,不该被解析成温度。

5.3 采样周期不是越大越好,但别小于数据手册要求

内部温度传感器采样时间不足,表现为读数明显偏低且随风速变化剧烈。你可以在 CubeMX 里把采样周期从 239.5 改成 13.5,观察adc_buf平均值是否掉。如果掉了 20 个 LSB 以上,说明采样时间不够,开关电容还没稳定就开始比较。反过来,如果两个采样周期读数差异很小,选更短的那个也不会带来明显精度损失,但温度响应会快一点。

一个客观验证:把调试器连接好,先记录 20 秒平均温度;再用热风枪吹 MCU 外壳,观察温度曲线是否在几秒后平滑上升。如果温度瞬间跳变几十度,多半是通道配置错了,采到了相邻引脚电平而不是内部传感器。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询