做了一年多TDLAS气体检测后,我最大的感受是:TDLAS这个词听起来很高端,可一旦把原理拆开,它本质上就是“激光穿过气体、光强被吸收了一点点、再想办法把这一点点测出来”。这篇文章就把从TDLAS原理到STM32实战的完整路径写出来,包括器件选型、硬件设计、调制解调代码和踩坑记录,给正在做毕业设计或者想把手头STM32项目往气体检测方向升级的朋友,提供一份可以直接抄的参考。
我手头这套方案用的是STM32F407VET6,激光器是1653nm的DFB蝶形管,配合InGaAs光电探测器,最终做到了甲烷气体的浓度显示和超限告警。整套代码是在Keil5 + STM32CubeMX HAL库环境下跑的。下面所有代码和电路描述都是我自己验证过的,但不同厂家的激光器和探测器参数有差异,抄作业的时候一定要按自己手上的器件重新标定。
1. 为什么选TDLAS做可燃气体检测,而不是传统催化燃烧/半导体
1.1 三种主流方案的比较
传统可燃气体检测,最常见的是催化燃烧式和半导体式。催化燃烧式本质上是让可燃气体在敏感元件表面燃烧,通过温度变化推算浓度。这种方案很成熟,但有两个让人头疼的问题:一是元件容易被硫、硅等物质毒化,寿命短;二是它检测的是“能烧的东西”,对甲烷、乙醇、氢气这些气体基本无法区分。
半导体式走的是表面吸附路线,气敏电阻遇到还原性气体后阻值变化。优点是便宜、灵敏,但选择性更差,环境湿度稍微一变,零点就飘得让人怀疑人生。
TDLAS的思路完全不同。它利用气体分子只在特定波长上有吸收峰这一点,直接让激光波长对准甲烷所在的吸收线。甲烷在1653.7nm附近有一条强吸收线,二氧化碳和水蒸气在这个区域干扰很小。所以TDLAS最大的好处是选择性好、寿命长、不需要频繁标定,响应速度也快。
| 对比项 | 催化燃烧式 | 半导体式 | TDLAS |
|---|---|---|---|
| 检测原理 | 燃烧放热 | 表面电阻变化 | 分子吸收光谱 |
| 选择性 | 差 | 差 | 好 |
| 寿命 | 2~3年,易中毒 | 中等,易受湿度干扰 | 激光器寿命长 |
| 成本 | 低 | 最低 | 高,但逐渐下降 |
| 响应速度 | 秒级 | 秒级 | 毫秒级 |
| DIY难度 | 低 | 低 | 中高 |
如果你只是想做个烟雾报警器之类的玩一玩,半导体式足够。但如果想认真做一个能检测ppm级甲烷的仪表,TDLAS是绕不开的方向。
1.2 TDLAS的基本原理和关键公式
TDLAS的理论基础是Beer-Lambert定律。激光穿过待测气体后,透射光强I和入射光强I0的关系可以写成:
I = I0 * exp(-σ(λ) * N * L)这里的σ(λ)是吸收截面,N是气体分子数密度,L是光程。浓度越高,光程越长,吸收越明显。对于低浓度气体,吸收很小,可以近似成:
ΔI = I0 - I ≈ I0 * σ(λ) * N * L也就是说,吸收光强和浓度近似成正比。这就是TDLAS能够定量测量的基础。
但实际做的时候没那么简单。DFB激光器的波长不是固定的,它主要靠驱动电流和管芯温度来调谐。电流调谐系数一般在0.01到0.03nm/mA,温度调谐系数大约0.1nm/K。所以我用DAC输出一个慢速三角波叠加高频正弦波,让激光器的波长在1653.7nm附近来回扫描。对应到采样信号上,吸收峰会变成周期性出现的凹陷。
为什么要用二次谐波检测?因为直接看“凹陷”很容易被激光器本身的功率波动和光路抖动干扰。我给激光器加一个高频正弦调制,当吸收峰被调制时,探测器信号里会出现调制频率的二倍频分量。这个2f分量的峰值对浓度非常敏感,而且能滤掉很多低频噪声。这就是所谓的WMS-2f方案,TDLAS传感器里最常用的信号处理方式。
打个比方,就像收音机调台:慢速旋转频率找台,同时手指轻微抖动,听信号强弱变化来判断是否对准了电台。二次谐波就是这个“信号强弱变化”被量化后的结果。
2. 系统总体设计与器件选型
2.1 光路结构:透射式还是反射式
自己做TDLAS,首先要决定光路。最常见的有两种:
透射式最简单,激光器出光后直接穿透气体,打到对面的探测器上。优点是光路好调,适合第一版验证原理。缺点是在同样浓度下,光程短,吸收信号弱。
反射式是通过反射镜让激光多次穿过气体,等效光程可以做到几十米甚至上百米,灵敏度大幅提升。但镜片对准非常痛苦,我调试Herriott池的时候,花了一整天才勉强把光斑对上探测器。
我的建议是:第一版老老实实做透射式,光程做到20cm到50cm,先把信号链跑通,再考虑反射池。
| 光路类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 透射式 | 结构简单,易对准 | 光程短,灵敏度有限 | DIY验证、教学演示 |
| 反射式 | 光程长,灵敏度高 | 光路调试困难 | 高端仪表、环境监测 |
| 开放光路 | 无需气室,可大范围检测 | 长光程下受环境湍流影响 | 天然气泄漏监测 |
2.2 核心器件清单和选型逻辑
我做这套系统用的器件清单如下:
- DFB蝶形激光器,中心波长1653.7nm,带TEC和热敏电阻,输出功率大概5mW。
- InGaAs光电探测器,工作波长900nm到1700nm,光敏面1mm。
- 跨阻放大运放,我用的是AD8628和OPA2376,偏置电流小,噪声低。
- STM32F407VET6,带2路12位DAC,内置ADC和多个定时器。
- 激光器恒流驱动电路,由运放、MOS管和精密采样电阻组成。
- TEC温控回路,负责把激光器管芯温度稳定在25℃。
选器件的时候有几个坑要提前避开。激光器一定选带TEC的蝶形封装,别图便宜买同轴封装,否则波长随温度漂到你怀疑人生。光电探测器要选InGaAs材料的,普通硅光电二极管对1653nm基本没有响应。
运放的选择也很关键。跨阻放大器的输入端是光电二极管的微弱电流,运放输入偏置电流比这个电流还大,那信号就完全被淹没在运放自身噪声里了。所以优先选低偏置电流、低噪声的运放。
2.3 为什么MCU选STM32F407而不是专用硬件
TDLAS检测设备在工业产品里经常用FPGA或DSP做数字锁相放大,成本高,开发周期也长。做DIY和毕业设计,STM32F407完全够用。
F407的主频是168MHz,带2路DAC、多路ADC、DMA控制器,还有一堆定时器,可以非常方便地把DAC波形输出和ADC采集同步起来。更重要的是HAL库资料多,出问题容易查。F103也能跑基础扫描,但它只有1路DAC,做双路波形输出会有点吃力,所以我最终选了F407。
3. 硬件电路与STM32接口设计
3.1 激光驱动电路:DAC怎么控制激光电流
DFB激光器的工作电流一般在20mA到80mA之间,STM32的DAC输出能力远不够直接驱动,所以中间必须加一级压控恒流源。基本结构是:DAC输出电压经过电压跟随器,送到恒流源的参考端,由运放控制MOS管调节流过激光器的电流。
电路的核心关系很简单:
I_LD = V_CTRL / R_senseR_sense是源极采样电阻。我用的R_sense是33Ω,DAC输出限幅在2.6V以内,对应电流约78mA,低于激光器80mA的绝对最大值。代码里生成波形时,会把归一化数值限制在0.17到0.98之间,就是为了防止DAC电压超限。
TEC温控是另一条独立链路。激光器模块内的热敏电阻接STM32 ADC,TEC通过一个H桥用PWM驱动,PID输出控制PWM占空比。TEC控温如果省略,短时间测试还能凑合,一旦环境温度变化,激光器波长就会偏移,吸收峰会直接消失。
3.2 光电检测与放大链路
光电探测器接收透过气体的激光后,产生微弱光电流,一般只有微安到纳安级别。这个电流需要经过跨阻放大器转换成电压,再进行放大和滤波,最后进STM32 ADC。
跨阻放大器的反馈电阻我用了1MΩ,输出灵敏度可以到1V/mA级别。反馈电容必须加,一般几皮法到几十皮法,否则高频段容易自激振荡。后面再接一级10倍放大和低通滤波,把带宽限制在2MHz以内,防止高频噪声混叠。
PD放大电路是整个系统里最容易出问题的地方。我踩过的坑是排线太长,导致输出波形叠了一堆50Hz工频干扰。后来把PD板子直接贴在主板上,用屏蔽线走模拟信号,噪声才压下去。
3.3 STM32引脚分配与GPIO配置
我用的引脚分配如下:
| 功能 | 引脚 | 说明 |
|---|---|---|
| DAC输出 | PA4 | 输出扫描+调制波形,控制激光驱动 |
| ADC输入 | PB0 | 采集PD放大后的信号 |
| TIM6 | 内部 | 触发DAC与ADC同步 |
| USART2 TX | PA2 | 向上位机发送浓度数据 |
| USART2 RX | PA3 | 接收指令 |
| 告警输出 | PC13 | 驱动蜂鸣器 |
| TEC PWM | PA8 | 控制TEC驱动器 |
GPIO配置时有个容易被忽略的细节:DAC和ADC引脚要配成模拟模式,别开上下拉,否则外部电阻会影响采样精度。如果用CubeMX生成,直接在Pinout里把PA4和PB0拖成DAC和ADC即可。
4. 核心代码:从HAL库到扫描与解调
4.1 生成激光扫描与调制波形
代码的第一步是生成一张wavetable,里面同时含有慢速三角扫描和高速正弦调制。我用的数组长度是2048,DAC更新频率是100kHz,所以一张表循环完是20.48ms,约49Hz的扫描频率。正弦调制周期在表里循环了32次,也就是说调制频率约为1.6kHz。
#define SCAN_POINTS 2048 #define SIN_CYCLES 32 uint16_t wavetable[SCAN_POINTS]; uint16_t adc_buf[SCAN_POINTS]; volatile uint8_t adc_done = 0; void WaveTable_Init(void) { for (int i = 0; i < SCAN_POINTS; i++) { float phase = (float)i / SCAN_POINTS; float tri = 1.0f - 2.0f * fabsf(2.0f * phase - 1.0f); float mod = 0.08f * sinf(2.0f * M_PI * SIN_CYCLES * phase); float value = 0.25f + 0.65f * tri + mod; if (value > 1.0f) value = 1.0f; if (value < 0.0f) value = 0.0f; wavetable[i] = (uint16_t)(value * 4095.0f); } }生成这张表的时候,要把DAC输出范围留出余量。0.25f + 0.65f * tri + mod的含义是:在0.25到0.9的基线的基础上,叠加快扫描三角波,再叠加上0.08幅度的正弦调制。这样既保证激光器不会过流,又让波长扫过吸收峰。
在CubeMX里,TIM6配置成定时器触发,设置好分频和自动重装值,让更新频率为100kHz。比如定时器时钟84MHz,PSC=4,ARR=167,更新频率就是84MHz / 5 / 168 = 100kHz。然后把DAC和ADC的外部触发源都选成TIM6的TRGO事件,这样DAC搬运下一个波形点和ADC采集当前点会严格同步。
4.2 ADC采集与二次谐波解调
启动DAC的DMA输出,同时启动ADC的DMA采集:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DAC_Init(); MX_TIM6_Init(); MX_ADC1_Init(); MX_USART2_UART_Init(); WaveTable_Init(); HAL_TIM_Base_Start(&htim6); HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t *)wavetable, SCAN_POINTS, DAC_ALIGN_12B_R); HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, SCAN_POINTS); while (1) { if (adc_done) { adc_done = 0; float v2f = 0.0f; LockInDetect(adc_buf, SCAN_POINTS, &v2f); float conc = Concentration(v2f); SendResult(conc, v2f); SetAlarm(conc); } } }ADC采集完成后进入回调函数,把标志位置1:
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { adc_done = 1; } }二次谐波解调函数的核心是正交锁相放大。按照2倍调制频率提取参考信号,分别与ADC数据做相关运算,然后求幅度:
void LockInDetect(uint16_t *buf, uint16_t len, float *out2f) { float X = 0.0f, Y = 0.0f, mean = 0.0f; for (int i = 0; i < len; i++) mean += buf[i]; mean /= len; for (int i = 0; i < len; i++) { float p = (float)i / len; float angle = 2.0f * M_PI * 2.0f * SIN_CYCLES * p; float val = (float)buf[i] - mean; X += val * cosf(angle); Y += val * sinf(angle); } X = 2.0f * X / len; Y = 2.0f * Y / len; *out2f = sqrtf(X * X + Y * Y); }为什么要在2f参考分量上做相关?因为信号里真正由吸收峰产生的谐波成分是调制频率的两倍,而激光器本身的光强变化主要在一倍频和低频段。乘上2f参考再积分之后,低频噪声会被平均掉很多。这就是锁相放大的威力。
这段代码在F407上跑没问题,但cosf和sinf在中断或高频循环里反复调用会占用时间。实际工程里我会提前算好一张cos和sin的查找表,把三角函数换成查表,速度能快好几倍。
4.3 浓度标定和温压补偿
解调出来的2f信号,和气体浓度之间并不是严格线性关系,但在低浓度段可以近似线性。强烈建议用标准气做多点标定,而不是直接套理论公式。
我做的标定数据大概是这样的:
| 甲烷浓度(vol%) | 2f信号幅度 |
|---|---|
| 0 | 0.012 |
| 0.5 | 0.087 |
| 1.0 | 0.158 |
| 2.0 | 0.305 |
| 5.0 | 0.710 |
用线性拟合后,浓度换算函数代码如下:
float Concentration(float v2f) { float conc_percent = 0.0068f * v2f - 0.0012f; if (conc_percent < 0.0f) conc_percent = 0.0f; return conc_percent; }这套系数只针对我手上这套光路。换了激光器、探测器或者光程,必须重新标定。对精度要求高的场合,还要做温度和压力修正。气体吸收的谱线强度随温度变化,分子密度随压力和温度变化,所以通用补偿公式可以简化成:
conc_true = conc_raw * (P_ref / P) * ((T + 273.15) / (T_ref + 273.15))这里P_ref和T_ref是标定时的基准压力和温度。如果只是做演示,这一步可以省略,但它直接影响户外环境下的长期稳定性。
4.4 数据输出与告警逻辑
最后把浓度和2f幅度通过串口发出去,方便上位机观察:
void SendResult(float conc, float v2f) { char msg[64]; int n = snprintf(msg, sizeof(msg), "CH4:%.2f%% V2f:%.4f\r\n", conc, v2f); HAL_UART_Transmit(&huart2, (uint8_t *)msg, n, 100); }告警逻辑很简单,浓度超过1%就拉低蜂鸣器引脚:
void SetAlarm(float conc) { if (conc >= 1.0f) HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); else HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); }这里注意PC13在大多数开发板上默认状态是高电平驱动LED,要想直接驱动继电器或蜂鸣器,建议加一级三极管或光耦,不要用引脚直接带大负载。
5. 常见问题与排查技巧实录
5.1 吸收峰找不到怎么办
这个问题我遇到太多次了。新手最常犯的错是直接按激光器手册上的中心波长去设电流,结果发现PD信号毫无变化。
首先判断DAC输出的扫描范围是否覆盖了吸收峰。1653nm附近的电流调谐范围可以先用2%甲烷标准气,慢慢手动增加DAC电压,同时看PD信号的谷底。如果没有标准气,可以用保鲜袋套住短光程气室,通入从打火机气罐收集的丁烷?不对,这里要测甲烷,所以最好还是买一瓶高纯甲烷或标准气。
如果曲线有凹陷但很浅,可能是激光器波长和吸收峰位置没对准,也可能是温控没稳住。先检查TEC温度是不是稳定在25℃,然后用波长计确认出光波长。
5.2 2f信号噪声大、底噪飘
2f信号最常见的问题不是信号没出来,而是底噪太高。我遇到过一个案例,2f波形像毛刺山一样,最后发现是开关电源的地线噪声串进了PD放大电路。
排查顺序是这样的:先用示波器看PD输出,正常应该是一条干净的扫描基线;如果上面有高频毛刺,优先怀疑电源和地线。STM32的ADC输入引脚不要和功率地混在一起,模拟地单独走线,最后单点接地。
其次检查DAC和ADC是否真的同步。如果不同步,锁相解调的参考相位会漂,2f信号会忽大忽小。最简单的验证方法是用一个已知峰值的模拟信号代替PD信号,看解调结果是否稳定。
5.3 温控振荡导致波长乱飘
TEC温控PID参数没调好时,输出会出现等幅振荡,激光器波长也跟着来回跳。表现是2f信号一会强一会弱,甚至完全无法锁定。
解决办法是先把PID参数调保守一点,让温控曲线只出现小的过冲,再用标准气验证吸收峰位置是否稳定。TEC的H桥驱动频率建议在20kHz以上,避免低频时电流噪声耦合进激光器供电。
5.4 串口乱码和工程编译问题
串口输出乱码,九成是系统时钟和CubeMX配置不一致。F407外部晶振是8MHz还是25MHz,必须在CubeMX里选对,否则波特率全部错位。Keil5打开工程前,记得先在Pack Installer里装好STM32F4系列的芯片包,不然Device列表里找不到F407。
| 故障现象 | 可能原因 | 排查方法 |
|---|---|---|
| 无吸收峰 | 波长没对准 | 扫全量程找谷底 |
| 2f噪声大 | 地线串扰、光路抖动 | 示波器查PD输出 |
| 信号漂移 | TEC温控振荡 | 调PID,确认温度反馈 |
| 串口乱码 | 时钟配置错误 | 检查外部晶振频率 |
| Dataloss/ADC缓冲错位 | DMA配置不对 | 查DMA buffer和传输方向 |
6. 调试心得与后续扩展
6.1 先标定,再谈算法
我一开始犯的错是上来就调算法,不停地改锁相放大的解调方式,结果2f信号还是不对。后来发现根本不是算法问题,是激光器电流和波长没有标定,吸收峰压根没被扫到。
所以我想强调一句:TDLAS项目的坑,百分之八十在光学链路和电流标定上,只有百分之二十在代码里。先把激光驱动、温控、PD放大这条模拟链路做稳,再写解调算法就顺理成章了。
6.2 后面还能怎么升级
这套基础版做完后,想继续深入可以从几个方向扩展。
一是换成双激光器方案,同时测甲烷和水的吸收线,用水蒸气谱线做辅助标定,抗干扰能力会强很多。二是把透射式光路升级成多次反射吸收池,光程从20cm变成几十米,检测下限能大幅降低。三是在F407上加入Modbus RTU或RS485输出,方便接PLC和上位机。如果性能还不够,再把锁相放大移植到FPGA上,但那已经是更复杂的项目了。
另外提一句安全方面的经验:激光器出光口调试时戴好护目镜,不要直视激光;测试可燃气体时务必在通风良好的环境下进行,浓度不要超过仪表的量程上限。无论你是做毕业设计还是准备量产,安全习惯都要从第一版就养成。
最后再分享一个小技巧:调试TDLAS时别急着上高深的谐波算法,先用最简单的DAC三角波扫描,把PD信号送到示波器上,亲眼看到甲烷吸收峰那个“凹陷”,再把调制和解调加进去。这个顺序能帮你省下一大半排查时间。