STM32 温度采样定时器触发配置示例
在嵌入式系统中,传统的软件轮询加延时方式采集温度数据存在隐患。例如主循环延时期间 CPU 无法处理其他任务,且多任务环境下难以保证精确的采样周期。为了解决这些问题,可以采用硬件协同的方式。
为什么非要用硬件触发?
对于需要高实时性、长期稳定运行的应用,软件轮询是行不通的。工业级系统要求确定性的行为:第 n 次和第 n+1 次采样的间隔必须严格相等。
STM32 的高级定时器(如 TIM2/TIM3)不仅可以计时,还能输出一个叫 TRGO(Trigger Output)的信号。这个信号可以像'发令枪'一样,精准地告诉 ADC:'现在开始转换!'整个过程不需要 CPU 参与,即使主程序正在处理 Wi-Fi 协议栈或者跑 FreeRTOS 的任务,也不会影响采样节奏。
定时器怎么当'发令员'?一步步带你配置
我们以 TIM3 为例,目标是让它每 1ms 发出一次 TRGO 脉冲,驱动 ADC 启动一次转换。
假设系统主频 72MHz,APB1 总线时钟也是 72MHz,我们要实现 1kHz 的触发频率(即周期 1ms)。
关键参数计算如下:
- 计数器时钟 = 72MHz / (PSC + 1)
- 目标更新频率 = 1kHz → 周期 = 1ms = 1000μs
- 所以:(PSC + 1) × (ARR + 1) / 72,000,000 = 0.001
取 PSC = 71 → 分频后时钟为 1MHz(每个计数 1μs) 取 ARR = 999 → 溢出时间 = 1000 × 1μs = 1ms
接下来设置 TRGO 信号源为'更新事件'(Update Event),这样每次溢出就会输出一个上升沿。
void TIM3_ConfigForADC(void) {
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
TIM_TimeBaseInitTypeDef timerInit;
timerInit.TIM_Period = 1000 - 1; // ARR
timerInit.TIM_Prescaler = 72 - 1; // PSC
timerInit.TIM_ClockDivision = 0;
timerInit.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &timerInit);
// 关键一步:选择主模式触发源为'更新事件'
TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update);
// 启动定时器
TIM_Cmd(TIM3, ENABLE);
}
这段代码执行完之后,TIM3 就开始自由运行了。它会自动每 1ms 打一枪,告诉 ADC:'该你干活了。'
注:除了更新事件,TRGO 还可以来自比较匹配、捕获等事件,适用于更复杂的同步场景。
ADC 准备好了吗?让它只听 TIM 的话
接下来轮到 ADC 登场。我们需要让 ADC 工作在'外部触发 + 单次转换'模式,并指定由 TIM3_TRGO 作为触发源。
STM32F1 系列中,ADC1 支持多个外部触发源,其中就包括 ADC_ExternalTrigConv_T3_TRGO。
此外,我们要采集的是内部温度传感器,它连接在 ADC 通道 16 上。使用前必须显式启用该功能。
void ADC1_TempSensor_Init(void) {
// 配置 ADC 时钟:通常建议不超过 14MHz
RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 72MHz / 6 = 12MHz
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE);
// 若使用外部 NTC 传感器,需配置对应引脚为模拟输入
GPIO_InitTypeDef gpioInit;
gpioInit.GPIO_Pin = GPIO_Pin_0;
gpioInit.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &gpioInit);
ADC_InitTypeDef adcInit;
ADC_StructInit(&adcInit); // 先初始化默认值
adcInit.ADC_Mode = ADC_Mode_Independent;
adcInit.ADC_ScanConvMode = DISABLE; // 单通道
adcInit.ADC_ContinuousConvMode = DISABLE; // 非连续,由外部触发控制
adcInit.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO; // 触发源
adcInit.ADC_DataAlign = ADC_DataAlign_Right;
adcInit.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &adcInit);
// ⚠️ 必须调用此函数才能启用内部温度传感器
ADC_TempSensorVrefintCmd(ENABLE);
// 配置规则通道:通道 16(温度传感器),采样时间尽量长些以提高精度
ADC_RegularChannelConfig(ADC1, ADC_Channel_16, 1, ADC_SampleTime_239Cycles5);
// 可选:执行 ADC 校准(推荐上电时做一次)
ADC_ResetCalibration(ADC1);
while (ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1));
ADC_Cmd(ADC1, ENABLE);
}
到这里,ADC 已经处于'待命'状态,只等 TIM3 的 TRGO 信号一到,立刻启动一次转换。
数据去哪了?让 DMA 默默搬走,别吵 CPU
想象一下:每 1ms 产生一个温度值,一天就是 86400 个数据。如果你每次都让 CPU 亲自去读 ADC_DR 寄存器,那它啥也别干了。
解决办法就是引入第三位主角:DMA(Direct Memory Access)。
我们配置 DMA,在每次 ADC 转换完成时,自动把结果从 ADC1->DR 搬到内存中的缓冲区。整个过程无需 CPU 干预,真正做到'采样归采样,处理归处理'。
而且我们可以开启循环模式(Circular Mode),当缓冲区满后自动从头覆盖,非常适合长期监控。
#define SAMPLE_BUFFER_SIZE 10
uint16_t adc_buffer[SAMPLE_BUFFER_SIZE];
void DMA_ConfigForADC(void) {
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_InitTypeDef dmaInit;
DMA_DeInit(DMA1_Channel1);
dmaInit.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; // 源地址
dmaInit.DMA_Memory0BaseAddr = (uint32_t)adc_buffer; // 目标地址
dmaInit.DMA_DIR = DMA_DIR_PeripheralToMemory; // 外设→内存
dmaInit.DMA_BufferSize = SAMPLE_BUFFER_SIZE; // 缓冲大小
dmaInit.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不变
dmaInit.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增
dmaInit.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
dmaInit.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
dmaInit.DMA_Mode = DMA_Mode_Circular; // 循环模式
dmaInit.DMA_Priority = DMA_Priority_High;
dmaInit.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_Init(DMA1_Channel1, &dmaInit);
// 使能 ADC 的 DMA 请求
ADC_DMACmd(ADC1, ENABLE);
DMA_Cmd(DMA1_Channel1, ENABLE);
}
这样一来,只要系统运行着,adc_buffer[] 就会被持续填充最新采样值。
你可以在 DMA 传输一半或全部完成时触发中断,进行批量处理:
void DMA1_Channel1_IRQHandler(void) {
if (DMA_GetITStatus(DMA1_IT_TC)) {
// Transfer Complete
float avg = 0;
for (int i = 0; i < SAMPLE_BUFFER_SIZE; i++) {
uint16_t raw = adc_buffer[i];
float voltage = raw * 3.3f / 4095.0f; // 转换为电压(12 位 ADC)
float temp = (voltage - 1.42f) / 0.0043f + 30.0f; // 查手册公式
avg += temp;
}
avg /= SAMPLE_BUFFER_SIZE;
// 发送到串口或 LCD
printf("Temperature: %.2f°C\r\n", avg);
DMA_ClearITPendingBit(DMA1_IT_TC);
}
}
提示:实际应用中建议加入滑动平均滤波或一阶 IIR 滤波,进一步平滑噪声。
系统架构图:三位一体的自动化流水线
最终系统的数据流清晰明了:
[TIM3] │ (每 1ms 发出 TRGO 信号)
▼
[ADC1] ← 内部温度传感器(Channel 16)
│ (转换完成)
▼
[DMA1] → 自动写入 adc_buffer[N]
└─▶ 半传输/全传输中断 → 温度计算 → 显示/报警/上传
这条链路由三个外设协同完成:
- TIM3:节拍控制器,提供精准时钟源;
- ADC1:感知单元,负责模数转换;
- DMA1:搬运工,悄无声息地转移数据。
CPU 唯一要做的事,就是在合适的时候看看结果,其余时间可以全力投入 PID 控制、网络通信、人机交互等核心业务。
实战经验分享:那些手册不会告诉你的坑
坑点 1:忘了开内部温度传感器供电
很多开发者发现读出来的一直是 0 或固定值,原因就是没调用:
ADC_TempSensorVrefintCmd(ENABLE);
这个函数不仅启用通道 16,还会打开内部参考电压路径,否则传感器没电!
坑点 2:采样时间太短导致精度下降
内部温度传感器阻抗较高,建议使用最长采样时间:
ADC_SampleTime_239Cycles5
否则可能因充电不足造成测量误差达±5°C 以上。
坑点 3:DMA 缓冲区未对齐或越界
确保 adc_buffer 数组长度与 DMA 配置一致,且避免在中断中频繁操作浮点运算(可在主循环处理)。
秘籍:如何获得更高精度?
ST 出厂时会在特定温度点(如 30°C 和 110°C)记录对应的 ADC 值,保存在芯片的 OTP 区域。你可以读取这些校准值进行线性修正,显著提升准确性。
例如:
// 假设已知:
// TS_CAL1 @ 30°C -> *(uint16_t*)0x1FFFF7B8
// TS_CAL2 @ 110°C -> *(uint16_t*)0x1FFFF7C2
int16_t raw_temp = adc_value;
float temp_c = ((float)(raw_temp - TS_CAL1) * (110.0f - 30.0f)) / (TS_CAL2 - TS_CAL1) + 30.0f;
这种架构适合哪些应用场景?
| 应用领域 | 是否适用 | 说明 |
|---|---|---|
| 电池管理系统(BMS) | ✅ 强烈推荐 | 需要长时间稳定采样,防止过热 |
| 电机驱动温控 | ✅ 推荐 | 实时性强,配合 PWM 同步采样 |
| 智能家电(空调、烤箱) | ✅ 适用 | 提升用户体验一致性 |
| 物联网终端节点 | ✅ 推荐 | 低功耗、少干扰 |
| 医疗设备恒温控制 | ✅ 高度推荐 | 对安全性和可靠性要求极高 |
相比之下,仅用于偶尔查看室温的小玩具项目,可以用软件轮询;但凡是涉及安全性、稳定性、实时性的工业级产品,这套方案几乎是标配。
总结与延伸思考
通过本文的讲解,你应该已经掌握了一套完整的、基于硬件协同的温度采样设计范式:
✅ 用定时器触发 → 解决时序抖动 ✅ 用 ADC 外部触发 → 实现非阻塞采集 ✅ 用 DMA 搬运数据 → 彻底解放 CPU
三者结合,构成了 STM32 嵌入式系统中最经典的'黄金三角'架构之一。
但这还不是终点。你可以在此基础上继续扩展:
- 改用多个 ADC + 多通道扫描,实现温度阵列监测;
- 动态调整 TIM 周期,实现自适应采样率(温变快时高频,稳态时低频);
- 结合 RTC 实现带时间戳的日志记录;
- 加入边缘计算逻辑,本地判断是否超温并触发保护动作。
嵌入式系统的魅力就在于:用最少的资源,做最可靠的事。
而这套定时器+ADC+DMA 的组合拳,正是通往高效、稳健系统设计的关键一步。

