拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32内部温度传感器+DMA采样:从CubeMX配置到滤波换算实践

简介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_IN16F2 / F4 系列对应ADC1_IN18G0 系列可能是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 时钟是 14MHz10µs 对应约 140 个 ADC 时钟周期那么 28.5 周期的采样时间根本不够至少要选 71.5 或 239.5 周期。ADC 总转换时间大约是采样周期加上 12 位逐次逼近的 12 个周期下表按 14MHz ADC 时钟计算ADC 时钟采样周期配置采样时间总转换时间12 位14MHz1.5 周期0.107µs0.964µs14MHz13.5 周期0.964µs1.821µs14MHz28.5 周期2.036µs2.893µs14MHz71.5 周期5.107µs5.964µs14MHz239.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°CVREF 不稳会更大F4 / F2ADC1_IN180x1FFF7A2C / 0x1FFF7A2E约 ±2°CG0 等查参考手册各型号不同需按手册确认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在右侧勾选IN16或IN18。注意是“ADC1”下的内部通道不是ADC1_INP16这种外部输入。如果 Pinout 图上原本被占用的引脚自动变成了ADC1_IN16把它点回Reset_State因为内部温度传感器不占物理引脚。接着按下面顺序操作将 ADC1 的Continuous Conversion Mode设为Enable。将DMA Continuous Requests设为Enable。在 DMA Settings 里添加ADC1方向选Peripheral To Memory模式选Circular。Memory Increment选择Enable目的是让 DMA 把连续转换结果按顺序写进数组。回到 Parameter Settings把Sampling Time选到 239.5 周期F4 或 F1 中对应宏可能不同单位都是 ADC 时钟周期。关键参数推荐值如下表配置项推荐值说明Clock PrescalerPCLK2 / 4 或 / 6让 ADC 时钟落在器件允许范围内F1 建议 ≤ 14MHzResolution12 位内部温度传感器不需要更高分辨率Scan Conversion ModeF1 需EnableF1 的 HAL 驱动在扫描模式下才能选内部通道 rankContinuous 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_16F4 展开后是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必须是 0history_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 用环境温度做单点偏移修正出厂校准值已经比直接用典型值准确很多但每块板子的电源、布局、外壳散热不同仍可能差 12°C。F4 系列可以在常温下用温度计读取环境温度和内部温度传感器输出比较得到cal_offsetint16_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 外壳观察温度曲线是否在几秒后平滑上升。如果温度瞬间跳变几十度多半是通道配置错了采到了相邻引脚电平而不是内部传感器。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门