简介:一个基于STM32的T12焊台控制完整项目,面向嵌入式入门者、电子爱好者与单片机开发者,可帮助学习从STM32CubeMX初始化、HAL库调用到ADC温度采集、PWM输出、按键与显示交互等完整开发流程,适合课程设计、毕业设计或温控类DIY项目参考。压缩包共116个文件,整体约800KB;核心代码为61个.h头文件与31个.c源文件,并配有IOC工程配置、Makefile/CMake构建脚本、PDF说明、Markdown笔记及drawio/emmx设计图,工程结构清晰,可导入STM32CubeIDE或Keil直接阅读。目前已有417人学习下载,适合正在开展T12焊台、温控系统或STM32综合项目的读者研究,可从工程中获取可复用的驱动代码和模块化组织思路。在代码阅读过程中,结合芯片手册,还能理解中断优先级配置、定时器PWM输出、ADC采样滤波等底层细节,提升嵌入式调试排错能力,是一份轻量实用的STM32实战范例。
1. 为什么是 STM32 来控 T12 焊台
很多人在入门烙铁 DIY 时会遇到一个尴尬:网上几十块的纯模拟 T12 控制板,温度一到就“嗒嗒嗒”继电器响,回温像过山车。而基于 STM32 的 T12 焊台,本质上是把一个“实时控制系统”装进了电烙铁里——用定时器输出 PWM 加热,用 ADC 在加热关断的瞬间采集热电偶电动势,再跑一个离散 PID 把温度钉在设定值上。这个标题的核心价值在于,T12 烙铁头内部加热丝与热电偶串在一条回路上,只有两种引线,所以“加热”和“测温”必须分时复用。STM32 的定时器触发 ADC、DMA 搬运、中断里更新 PID 输出,正好能把微秒级的测量窗口利用起来。适合想把手边 T12 手柄升级成数字焊台的人,也适合把 HAL 库和标准库实战串起来学的嵌入式工程师。
2. T12焊台硬件方案与关键器件选型
2.1 主控选择:为什么 F103 和 G0 都有人用
常见的做法是 STM32F103C8T6 或 STM32G030F6P6。F103 因为“研究生开发板”普及率极高,资料多,用标准库照着都能搭出来;G0 系列胜在价格更便宜,而且 TIM1 同样支持 ADC 触发。关键不在于算力,而在于定时器结构——必须有一个定时器能产生 PWM,同时在 PWM 低电平期间产生 ADC 触发事件。F103 的高级定时器 TIM1/TIM8 都支持,普通定时器 TIM2-TIM4 配合主从模式也能做。选型时可以看这张表:
| 主控型号 | ADC 通道 | 高级定时器 | 参考价格(散片) | 备注 |
|---|---|---|---|---|
| STM32F103C8T6 | 3 个 ADC,共 10 路 | TIM1/TIM8 | 约 5 元 | 标准库/HAL 资料最多 |
| STM32G030F6P6 | 1 个 ADC,共 8 路 | TIM1 | 约 2 元 | TSSOP20 体积小 |
| STM32G431CBT6 | 3 个 ADC,带内部运放 | TIM1/TIM8 | 约 8 元 | 内部运放可省掉外部放大 |
如果你手里只有 F103 的最小系统板,直接按 F103 做就行。硬件设计时注意把 ADC 输入引脚、PWM 输出引脚、I2C 引脚留出来,G0 也能参考同一套电路。极端情况下 APM32 这类无缝兼容芯片也能直接烧录 STM32 的程序,工程配置不用改引脚。
2.2 加热驱动:MOSFET 和 PWM 频率的配合
驱动部分用一只 N 沟道 MOS 做低边开关,负载接 PMOS 还是 NMOS 取决于你的电源和地线规划。多数 DIY 焊台用 N-MOS:24V 电源正极直接接手柄其中一根线,另一根线接到 MOSFET 漏极,源极接地。栅极由 STM32 定时器输出的 PWM 经过 MOS 驱动电路后驱动。
PWM 频率建议选 20kHz 以上,一方面避开人耳可听范围,另一方面减小电感啸叫。T12 发热丝等效电阻约 8 欧姆(冷态),24V 供电时峰值电流约 3A,选管子时关注Rds(on)和Vgs(th):
| 参数 | 典型值 | 说明 |
|---|---|---|
| Vgs(th) | 1~2V | 3.3V 逻辑电平能不完全导通,建议加驱动 |
| Rds(on) | 10~50mΩ | 越低,发热损耗越小 |
| 漏源耐压 | 30V 以上 | 留 50% 余量 |
| 总栅极电荷 Qg | 10~30nC | 影响开关损耗 |
我一般用 IRLZ44N 或者 AOD4184,前者便宜好买,后者贴片方便。PWM 频率不要追求一味的 100kHz,频率越高,MOS 开关损耗越大,驱动电阻也要重新匹配,反而可能炸管。
2.3 热电偶放大:不是直接接 ADC 就行
T12 烙铁头在室温下热电偶电动势只有零到几毫伏,加热到 400 度时大约 10 到 16 毫伏。直接接 STM32 ADC 会被分辨率坑死,因为 12 位 ADC 在 3.3V 参考下的 LSB 是 0.8mV,连 10 度都分不出来。所以必须放大。
常见的信号链是:手柄两线分压后,经过仪表放大器或运放放大到 0~3.3V 范围内。注意 T12 发热丝冷态电阻小,如果只是单纯测热电偶两端电压,在加热期间会因为加热电流在发热丝上产生压降,测到的是“热电势 + 压降”的混合信号。这就是为什么必须在 PWM 关闭期间采样——关断瞬间加热电流为 0,热电偶两端电压就是纯热电势。
运放选型上,LM358 做单电源放大,在毫伏级会有不小的失调电压,可能让常温下 ADC 输出飘到 5 度以上。预算允许可以换 OP07 或 LMV358,前者的低失调电压(最大 75uV)很适合热电偶放大。电路上建议采用差分结构,把两根线分别接入运放的输入,避免地线压降混进去。放大倍数定在 100 倍,对应 3.3V ADC 满量程,最高可测约 550 度,覆盖 T12 常用温度上限。
2.4 供电与地线布局
整个系统需要 24V(加热)、5V(逻辑部分或编码器供电)、3.3V(MCU)。常见方案是 24V 直流输入,用开关电源模块降到 5V,再经 AMS1117 得到 3.3V。这个链路没有任何坑,但地线布局直接决定 ADC 稳定程度。
加热回路的大电流地线必须和 MCU 模拟地单点连接,最好隔开几毫米再汇到电源地。热电偶放大部分的地如果和加热 MOS 地共用一条长走线,PWM 开断时地电位跳动可能达到几十毫伏,最终显示温度跳来跳去。我习惯把模拟放大电路放在 PCB 一角,用 0 欧电阻或磁珠把模拟地和功率地连在一起。
3. 基于 STM32 的 T12 焊台软件实现
3.1 从 STM32CubeMX 生成工程还是用标准库
这里有两个方向:给 F103 用标准库,或者用 STM32CubeMX 生成 HAL 工程。热词里有人搜“keil5安装stm32芯片包”,说明很多人已经在用 Keil 加 STM32CubeMX。我推荐用 CubeMX 生成基础工程,再手动改定时器和 ADC 配置,因为 HAL 库对 ADC 触发和 DMA 的封装比较友好,标准库则需要自己翻寄存器。初始化时开启 TIM1 的 PWM 输出,ADC 触发源选择定时器触发,DMA 配置成循环模式。核心代码如下:
// CubeMX 生成代码后,在主循环前配置 TIM1 更新中断和 ADC 触发 HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); // 启动 PWM 加热输出 HAL_TIM_Base_Start_IT(&htim1); // 开定时器中断,用于采样时序 HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, 64); // 启动 ADC DMA 采集逻辑说明:HAL_TIM_PWM_Start让定时器持续输出控制温度用的 PWM;HAL_TIM_Base_Start_IT使能更新中断,在中断里可以做采样窗口判断;HAL_ADC_Start_DMA启动后,固件会持续把 ADC 转换结果搬进adc_buf。参数64是 DMA 缓冲长度,配合循环模式可以得到一个滑动窗口,便于之后做滤波。
3.2 在 PWM 关断窗口里同步采样
T12 测温最关键的软件逻辑是“不能在加热时测”。实现方法很多,我习惯在 TIM1 更新中断里判断当前 PWM 的电平状态。如果 PWM 为低,就立刻触发一次 ADC 转换,转换完成后通过 DMA 中断把值读出来。伪代码如下:
void TIM1_UP_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(&htim1, TIM_FLAG_UPDATE)) { __HAL_TIM_CLEAR_FLAG(&htim1, TIM_FLAG_UPDATE); if (HAL_GPIO_ReadPin(PWM_PORT, PWM_PIN) == GPIO_PIN_RESET) { // PWM 低电平,此时加热丝无电流,采集热电偶电压 HAL_ADC_Start_IT(&hadc1); } } }逻辑说明:HAL_GPIO_ReadPin用来读 PWM 引脚当前电平,这种方法要求 PWM 周期内至少有一段时间持续为低,不能是 100% 占空比。参数上没有特别讲究,但要在 CubeMX 里把 TIM1 的Pulse初始化成 0,避免上电瞬间默认满占空比把烙铁头烧坏。更稳的方案是用定时器主从模式,让 PWM 低电平触发 ADC 硬件点信号,但会复杂不少,DMA+中断已经够用。
ADC 数据是带噪的。T12 热电偶信号放大后,叠加上 MOS 开关、电源纹波、运放偏置,直接取单次值会跳。我一般对 DMA 缓冲里的最近 8 次数据做均值滤波,再送 PID。如果想让温度显示更平滑,可以改成滑动窗口滤波,但要注意滞后不要太强,否则会掩盖回温过冲。
3.3 温度计算与查表线性化
放大电路输出到 ADC 的电压和温度不是线性关系。T12 烙铁头内部热电偶在 0~400 度范围内的电势变化大约每度几十微伏,但曲线有弯曲。理论上可以直接用热电偶分度表查表,但为了精简代码,也可以把已知的电压-温度点做成两个数组,用线性插值找出温度。
// 电压单位 mV,温度单位 ℃ const float temp_table[] = {0, 50, 100, 150, 200, 250, 300, 350, 400}; const float volt_table[] = {0.0f, 2.5f, 4.9f, 7.3f, 9.7f, 12.1f, 14.5f, 16.8f, 19.1f}; int16_t adc_to_temp(float volt_mv) { if (volt_mv <= volt_table[0]) return (int16_t)temp_table[0]; for (int i = 1; i < 9; i++) { if (volt_mv <= volt_table[i]) { float ratio = (volt_mv - volt_table[i-1]) / (volt_table[i] - volt_table[i-1]); return (int16_t)(temp_table[i-1] + ratio * (temp_table[i] - temp_table[i-1])); } } return (int16_t)temp_table[8]; }上面表里的电压值是示意,实际要根据你的运放放大倍数和电阻分压重新标定。插值代码用float运算会稍微占用内核,但 F103 主频 72MHz 完全够用。注意数组按温度升序排列,否则二分和顺序查表都会出错。
3.4 PID 控制器代码
温度控制我选位置式 PID,输出是 0~100% 的功率。因为 T12 热惯性不大,位置式比增量式写起来更直观,限幅也方便。直接在定时器中断里调 PID,输出更新 PWM 占空比:
typedef struct { float kp, ki, kd; float integral; float last_error; float max_output; } pid_t; float pid_update(pid_t *pid, float setpoint, float measurement) { float error = setpoint - measurement; pid->integral += error; if (pid->integral > 100.0f) pid->integral = 100.0f; if (pid->integral < -100.0f) pid->integral = -100.0f; float derivative = error - pid->last_error; pid->last_error = error; float output = pid->kp * error + pid->ki * pid->integral + pid->kd * derivative; if (output > pid->max_output) output = pid->max_output; if (output < 0.0f) output = 0.0f; return output; }逻辑说明:integral积分项累加误差,同时限幅到正负 100,防止积分饱和;derivative用的不是误差对时间的导数,而是误差的差值,在固定周期内调用时比例关系差不多。输出限幅到 0~max_output,避免冷态开机时 PID 给满功率导致温度冲过头。max_output建议先设 80%,等实测没问题再放开。
控制周期取多少?常见做法是 100ms 跑一次 PID,也就是定时器中断里每 10 次采样才更新一次 PWM 占空比。T12 发热体热时间常数在秒级,100ms 控制周期足够,还能减少高频干扰对积分项的影响。
3.5 人机交互:旋转编码器和 OLED
显示用常见的 0.96 寸 I2C OLED,128x64 分辨率。旋转编码器 EC11 用来调温度,按下编码器切换“温度设定”和“菜单”。软件上要处理好编码器的正交信号,最简单的办法是用 EXTI 中断读另一相的电平判断方向。
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == ENC_A_PIN) { if (HAL_GPIO_ReadPin(ENC_B_PORT, ENC_B_PIN) == GPIO_PIN_RESET) { set_temp += 5; // 逆时针降低 5 度 } else { set_temp -= 5; // 顺时针升高 5 度 } if (set_temp > 420) set_temp = 420; if (set_temp < 100) set_temp = 100; } }注意这里把 A 相当作下降沿触发,B 相电平判断旋转方向,EC11 的按下键可以另外接一个普通 GPIO 做低有效输入。set_temp的上下限根据你的手柄耐温设置,不要写成全局到处改,否则逻辑容易乱。
OLED 刷新放在主循环,不要放在中断里。显示内容至少包括当前温度和设定温度,如果空间够,再画一个动态功率条。实际调试时功率条比温度数字更有用,能直观看出 PID 是否在振荡。
4. 参数怎么设:PID、采样与 PWM 联动调优
4.1 从零开始试凑 PID 参数
上一章给出的代码只是骨架,真正让它好用的是一组合理的 PID 参数。很多人上来就套别人的Kp=50, Ki=5, Kd=2,结果一焊就超温或回温慢。原因是控制周期、ADC 滤波等级和 PWM 频率都会改变系统传递函数,参数要跟着自己的硬件重新调。
先看下表,了解三个参数对温度曲线的作用:
| 参数 | 调大的效果 | 调太大会怎样 | 参考范围(100ms 周期) |
|---|---|---|---|
| Kp | 升温明显变快 | 严重超调,到达设定值后震荡 | 10~50 |
| Ki | 消除稳态误差 | 回温时过冲,稳定后小幅振荡 | 0.5~5 |
| Kd | 抑制过冲和回温尖峰 | 对噪声敏感,温度数字跳得厉害 | 0~10 |
初始调节步骤:把 Ki、Kd 设为 0,从 Kp=10 开始,设定 300 度观察温度曲线。如果温度距离设定值还很远就开始过冲,就调低 Kp;如果到达设定值但误差一直存在,再慢慢加 Ki。每加一次 Ki,都要回到 Kp 重新看动静。
一个常见错误是把 PID 输出直接当 PWM 占空比,然后拿 PID 的 D 项去压振荡。实际 T12 焊台的温度传感器带宽很低,D 项对高频噪声敏感,容易让 MOS 驱动一抖一抖的。我习惯在温度值送到 PID 前先做一阶低通滤波,或者干脆把 D 项设成 0,只用 PI 控制。对大多数焊台来说,PI 已经够好,D 项只是锦上添花。
4.2 采样时刻和 PWM 占空比的冲突
T12 的测温窗口是 PWM 低电平期间,但如果占空比超过了某个阈值,低电平时间就会太短,ADC 采样还没完成,PWM 又拉高了。典型的 PWM 20kHz,周期 50 微秒,STM32 ADC 转换时间可以配置到 1 微秒左右,但加上运放建立时间,实际能在低电平期间空出 10 微秒以上。如果占空比超过 90%,低电平时长就小于 5 微秒,温度采样变得不可靠。所以你在代码里要把最大占空比限制在 80% 左右。
假如你需要更高的加热功率,比如快速回温时想占空比拉到 95%,那就要改变采样策略:不需要每个 PWM 周期都采样,可以每 10 个周期腾出一个完整的低电平窗口来做采样。实现上可以加一个sample_skip_counter,在计数器到某个阈值时强制把 PWM 拉低一个固定时间,再做 ADC 转换。这会引入轻微功率损耗,但换取了可靠的采样。
4.3 冷态启动和发热状态下的限幅策略
冷烙铁头阻值低,上电瞬间如果 PID 输出 100% 占空比,即使只有 24V,电流也可能超过 5A。电源模块扛得住,MOS 管结温会瞬间变化。所以启动时要加软启动逻辑:刚开始每 100ms 只允许输出比上次高 1% 的功率,直到达到目标输出。说白了就是一个斜坡限幅。
代码里可以在 PID 输出之后再加一个功率斜率限制:
static float last_output = 0.0f; float ramped_output = last_output; if (pid_out > last_output + 1.0f) { ramped_output = last_output + 1.0f; } else if (pid_out < last_output - 5.0f) { ramped_output = last_output - 5.0f; } else { ramped_output = pid_out; } last_output = ramped_output; __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, (uint32_t)ramped_output);参数说明:上升速度 1%/100ms,让冷态烙铁头慢慢热起来;下降速度放宽到 5%/100ms,避免回温到设定值后输出掉得太慢导致过冲。这两个斜率不是绝对标准,如果你的电源输出能力强,可以把上升斜率改成 2;如果用的是额定电流很小的电源,就得再调小。
4.4 硬件噪声导致的温度值“抖动”
如果你发现 OLED 上的温度在不用风枪吹时也会来回跳 5~10 度,首先查的是 ADC 参考电压。板子如果直接用 3.3V LDO 给 VDDA,而 LDO 前面又和加热 MOS 共用电源,那 PWM 一开,VDDA 就会跟着波动。解决办法是在 VDDA 引脚前加一个 1uH 磁珠和 10uF 电容,组成 π 型滤波。
其次是热电偶放大电路的 PCB 走线。T12 手柄两根线如果和 PWM 信号线并行走线超过 3 厘米,开关噪声会耦合成几毫伏的干扰。我一般把放大电路和 MCU 放在同一侧,手柄线从板边直接进入运放引脚,中间不要打过孔。这类问题有时候和 PID 参数无关,调参再久也压不掉。
5. 让 T12 焊台更可靠:校准与验证技巧
5.1 两点校准,比换“精准”运放更有效
即使选了精密运放,电阻误差也会让显示温度偏离真实值。常见做法是准备一个工业温度计或测温枪,把烙铁头放在测温点附近,分别在 200 度和 350 度做两点校准。软件里保存一个线性校正系数,实际上就是调整表里的温度偏移和增益。公式很简单:
calibrated_temp = offset + scale * raw_temp; // offset 和 scale 分别由两点实测差值计算如果你只有一路 ADC,校准过程要快,防止烙铁头温度漂移。建议把校准逻辑放在 OLED 菜单里,进入校准模式后设定到目标温度,等稳定 10 秒后按键确认,用两次确认的数据算offset和scale。
5.2 用串口跑 PID 曲线,不用显示来猜
OLED 刷新率太低,看温度变化只能看到结果。调试时我通常把当前温度、设定温度、PID 输出通过 USART1 以 10Hz 频率发出去,在电脑上用串口绘图软件或者简单的 Python 脚本画曲线。这个方法能一眼看出过冲、往复振荡和积分饱和。
import serial import matplotlib.pyplot as plt ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=1) temps = [] setpoints = [] outputs = [] for _ in range(300): line = ser.readline().decode().strip() if line.startswith('T:'): parts = line.split(',') t = float(parts[0].split(':')[1]) s = float(parts[1].split(':')[1]) o = float(parts[2].split(':')[1]) temps.append(t) setpoints.append(s) outputs.append(o) plt.plot(temps, label='temp') plt.plot(setpoints, label='setpoint') plt.plot(outputs, label='pwm') plt.legend() plt.show()脚本先读串口 300 行,每行格式固定成T:315,S:320,P:45。绘图后如果温度线在设定线附近反复穿越且幅度超过 10 度,说明 Kp 偏大或 Ki 偏大。如果温度线缓慢爬升且永远差几度到不了位,说明 Ki 太小或积分限幅太紧。
5.3 不要省掉看门狗和掉电写入
T12 焊台的控制逻辑如果因为干扰跑飞,PWM 输出会停在某个占空比上,轻则过温,重则烧手柄。所以项目里至少加一个独立看门狗 IWDG,在主循环里周期喂狗。如果主循环卡死,看门狗复位后程序重新初始化,PWM 恢复为 0,烙铁头会自然冷却。
设定温度最好保存在 Flash 里。STM32 内部 Flash 写次数有限,不能每次按键都写。建议只在停止使用或按下“保存”按键时写一次。读取时做合法性检查,比如不在 100~420 范围内就恢复默认值。这样即使set_temp被调试时改成奇怪的值,重启后也不会带着跑飞的数据继续工作。最后留一个保险:如果热电偶采样超过 600 度对应的电压,立刻切断 PWM 并显示错误码,这样失控的加热能在被察觉前终止。
本文还有配套的精品资源,点击获取