
光功率计这玩意儿玩光通信、做激光实验、调光路的朋友应该都不陌生。以前我总觉得它是专业仪器动不动几千块直到有一天需要定量测激光笔功率实在下不去手买贵的就自己花几十块钱的材料搭了一个。实测下来测个红光激光笔、LED光源的输出功率量级完全能对上稳定性也够用。这篇文章不打算一遍遍重复光电探测的原理公式原理网上到处都是我重点讲怎么把图纸变成实物、怎么把代码烧进去、怎么校准才能让读数靠谱。全文涉及电路图、程序代码、元器件选型和踩坑记录参考着做一台完全没问题。1. 整体设计与思路拆解1.1 先把需求定清楚再想电路方案做东西之前最忌讳一上来就找电路图。我的习惯是先列需求这光功率计要测什么光源功率大概什么量级要不要换挡供电方不方便以我这次的需求为例主要测650nm红光激光笔毫瓦级、红色LED微瓦到毫瓦级希望显示精度在0.01mW左右能实时刷新成本控制在50元以内。围绕这几个条件才会考虑核心器件到底是选光电二极管、光电池还是光敏电阻。很多人会纠结光敏电阻但光敏电阻响应速度慢、线性度差定量测量基本不靠谱。硅光电池灵敏度高但内阻低直接接放大器负载影响明显。最后我选的是PIN光电二极管BPW34它响应速度快、暗电流小、在可见光和近红外波段响应不错最关键的是便宜一片几块钱用来做简易光功率计非常合适。1.2 方案对比为什么选“光电二极管跨阻放大器”而不是直接ADC网上很多低成本方案是把光电池直接接Arduino的ADC引脚省去运放。我试过读数确实有变化但有两个硬伤一是线性范围窄光源稍微强一点就饱和二是输出阻抗匹配不好ADC输入电容和引脚漏电会让读数飘得厉害尤其在微瓦级别。正确的做法是让光电二极管工作在光伏模式配合一个高阻值的反馈电阻组成跨阻放大器TIA。光电二极管产生的光电流流过反馈电阻在输出端变成电压电压值就等于光电流乘以反馈电阻。这样做的好处是4点输出电压可以直接给ADC读信号源阻抗低。增益靠反馈电阻调节换量程只需切换Rf。运放的两个输入端虚短光电二极管两端的电压几乎为零暗电流和漏电会小很多。即使微弱光信号也能通过加大Rf放大到可测范围。这个方案里放大器选型很关键。我一开始用了LM358后来发现输入端偏置电流太大高阻反馈下输出零点漂移非常明显换了TL081之后明显改善。如果想把功耗和噪声再压低可以用OPA2376这类轨到轨精密运放但价格会高一些。对学生党或普通玩家来说TL081足够。1.3 系统架构和关键参数估算整体架构从前往后走一遍就是光电二极管接收光→跨阻放大器把光电流变成电压→Arduino内部ADC采样→软件滤波、显示数据。估算参数之前需要明白BPW34的几个关键指标短路电流约1.4μA/100lx这个和光功率换算需要逐步标定暗电流典型值2nA一般可忽略响应波长范围430nm到1100nm峰值在860nm附近650nm处响应度大约0.4A/W以测1mW红光为例光电流I0.4A/W×1mW0.4mA。如果直接用1kΩ反馈电阻输出电压只有0.4V太小所以要换大阻值。测毫瓦量级反馈电阻取10kΩ到100kΩ比较合理测微瓦量级用1MΩ到10MΩ。我最后用了一个多档切换的设计反馈电阻在100kΩ、1MΩ、10MΩ之间选择这样从微瓦到毫瓦都能覆盖。2. 元器件选型与材料准备2.1 核心元件清单直接在图纸上画之前先把菜买齐。以下是完整清单我标注了大概价格和选择理由。元件型号/规格数量说明光电二极管BPW341核心探测器可见光到近红外响应运放TL081CN1高输入阻抗JFET输入适合跨阻放大反馈电阻100kΩ、1MΩ、10MΩ各1三档量程切换精度1%以上补偿电容10pF22pF1并联在反馈电阻上抑制振荡保护电阻100Ω1串联运放输出端防倒灌电位器10kΩ多圈1校准满量程时微调增益单片机Arduino Nano1便宜好写ADC是10bit显示屏SSD1306 OLED 0.96寸 I2C1显示功率值电源模块AMS1117-5.0或手机充电头1给Arduino供电5V稳压稳压管78L051可选给运放单独供电更稳其他面包板/PCB、杜邦线、端子若干搭建实验电路这套下来大概40元左右不算贵。如果手头已经有Arduino和OLED还能再省一点。2.2 反馈电阻值怎么计算选电阻不能拍脑袋要结合量程反推。以10MΩ档为例满量程假设输出2.5V给Arduino ADC留一半余量避免接近供电轨那么对应光电流I2.5V/10MΩ250nA。650nm红光下BPW34响应度约0.4A/W对应光功率P250nA/0.4A/W625μW。也就是说10MΩ档适合测几十微瓦到小几百微瓦。1MΩ档满量程对应光电流2.5μA光功率约6.25mW适合测红光激光笔、亮一些的LED。100kΩ档满量程对应光电流25μA光功率约62.5mW适合测较强的汇聚光。三档覆盖从微瓦到几十毫瓦基本能满足绝大多数业余场景。如果后续要测更弱的光把反馈电阻换成100MΩ即可但这时对电路板的绝缘要求急剧上升焊盘的灰尘潮气都会造成漏电后面我会专门讲怎么处理。2.3 关于供电和LDO的选择Arduino Nano可以直接用USB供电但TL081运放的电源轨有讲究。如果从USB的5V供电TL081在5V单电源下输出摆幅只能到大约3.5VADC的有效测量范围只剩0到3.5V有点浪费。更稳妥的做法是用9V电池或两节18650串联经78L05稳压出5V给Arduino再给运放提供±5V左右的双电源。TL081是双电源运放±5V供电时输出摆幅很充裕。如果实在想省事单电源5V也能工作但需要把光电二极管的负极接到2.5V虚地电路会复杂一些。我的建议是新手老老实实用双电源或者直接选轨到轨运放单电源供电一劳永逸。3. 电路连接与搭建要点3.1 跨阻放大电路接线图下面用文字把连接关系描述清楚照着接线就行。光电二极管D1: 阳极(A) - GND 阴极(K) - TL081的2脚反相输入端 TL081运放: 2脚(IN-) - 光电二极管阴极 3脚(IN) - GND 6脚(OUT) - Arduino A0引脚 7脚(V) - 5V 4脚(V-) - -5V 反馈网络: 反馈电阻Rf100k/1M/10M切换: 一端接2脚(IN-) 一端接6脚(OUT) 补偿电容Cf10pF: 并联在Rf两端这里需要注意光电二极管的连接方向很多人会接反。光伏模式下面光电二极管工作在零偏压阳极接地阴极接运放反相输入端这个方向绝对不能错接反了输出会直接饱和到负轨或正轨。单电源供电时TL081的4脚接GND3脚接一个2.5V的偏置电压两个10k电阻分压这样输出以2.5V为中间点没有光时输出大约2.5V有光时往上或往下变化。3.2 量程切换电路的实现切换反馈电阻我用的是一个单刀三掷的波段开关或者直接在面包板上插拔跳线帽。手头没有开关的话插拔也算切换方案缺点是麻烦点。切换时有个坑反馈电阻切换的瞬间运放输入端是悬空的输出会瞬间飞出可能把ADC引脚打坏。所以切换时应先断开光电二极管输入或者把Arduino的A0引脚先配成输入模式高阻态切换完再接回来。如果不做软保护我就在开关触点并联一个100kΩ固定电阻这样即使开关断开运放也有一个确定的反馈路径不会飘到天上去。3.3 元器件的绝缘防漏电处理做高阻抗跨阻放大时电路板干净程度直接影响测量精度。10MΩ反馈电阻意味着输入端微小的漏电流都会产生很大的电压误差。漏电流的主要来源有三个焊盘之间的助焊剂残留面包板内部金属弹片的氧化和污垢空气潮湿时PCB表面形成的薄水膜我的经验是如果只是面包板验证把反馈电阻和光电二极管的引脚架空尽量不要贴紧面包板表面。如果做长期用的正式板子焊完之后用洗板水或无水乙醇彻底清洗焊盘区域最后涂一层三防漆。另外反馈电阻用金属引脚直插式比较好贴片电阻在高湿环境下的漏电路径更短更容易出问题。3.4 屏蔽和接地光电二极管对空间环境的电磁干扰比较敏感尤其是周围有手机充电器、开关电源的时候。我这次实测过程中把手机放近一点读数就会跳。解决方式很简单给光电二极管和反馈电阻盖一个金属屏蔽罩外壳接地。接地也有讲究运放的地和Arduino的AGND要单点连接不要在地线上形成环路。电源线尽量短粗避免在天线上捡噪声。跨阻放大器属于高灵敏模拟前端屏蔽和接地做好了读数能稳定一个数量级。4. 代码实现与校准方法4.1 Arduino代码从ADC采样到OLED显示代码思路不复杂Arduino读A0的ADC值通过滑动平均滤波再根据当前量程对应的标定系数换算成功率值最后显示在OLED上。先看完整代码块后面对重点部分做解释。#include Wire.h #include Adafruit_SSD1306.h #include Adafruit_GFX.h #define ADC_PIN A0 #define OLED_WIDTH 128 #define OLED_HEIGHT 64 Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, Wire, -1); // 量程档位: 010MΩ, 11MΩ, 2100kΩ int rangeIdx 1; long feedbackResistor[] {10000000L, 1000000L, 100000L}; // 对应650nm处的响应度: 0.4 A/W // 换算系数 1 / (响应度 * 反馈电阻) [单位为 mW / 电压V] // 10MΩ : 1 / (0.4 * 10000000) 0.00025 V? 不对这里需要推导见下文说明 double coeff[] {0.00025, 0.0025, 0.025}; // 单位: mW / ADC电压(1V?) // 滑动平均滤波 const int numReadings 10; double readings[numReadings]; int readIndex 0; double total 0; double average 0; void setup() { Serial.begin(9600); if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println(OLED init failed); for (;;); } display.clearDisplay(); for (int i 0; i numReadings; i) { readings[i] 0; } } double readVoltage() { int sensorValue analogRead(ADC_PIN); double voltage sensorValue * (5.0 / 1023.0); return voltage; } double calcPower(double voltage, int range_sel) { // 光功率P(mW) 输出电压V / (响应度RA * 反馈电阻Rf) // RA 0.4 A/W at 650nm // 例如1MΩ档输出1V则光电流 1V / 1MΩ 1e-6 A // 光功率 1e-6 / 0.4 2.5e-6 W 0.0025 mW double power_mW voltage / (0.4 * feedbackResistor[range_sel]) * 1000.0; return power_mW; } void loop() { total total - readings[readIndex]; readings[readIndex] calcPower(readVoltage(), rangeIdx); total total readings[readIndex]; readIndex (readIndex 1) % numReadings; average total / numReadings; display.clearDisplay(); display.setTextSize(2); display.setCursor(0, 0); display.println(average, 3); display.setTextSize(1); display.setCursor(0, 32); display.print(mW R); display.println(feedbackResistor[rangeIdx] / 1000000); display.display(); Serial.print(Power: ); Serial.print(average, 4); Serial.print( mW, ADC: ); Serial.println(analogRead(ADC_PIN)); delay(200); }4.2 代码里系数是怎么推出来的代码里的calcPower函数是实现准确测量的核心。这里展开推导一下跨阻放大器的输出电压UoutI×Rf。I是光电二极管产生的光电流。光电二极管在特定波长的响应度RA表示每瓦光功率产生多少安培电流单位A/W。所以IRA×P其中P是光功率单位瓦。综合起来就是UoutRA×P×Rf。反推功率就是PUout/(RA×Rf)。由于我要显示mW单位而Uout单位是VRf单位是ΩRA单位是A/W结果是W再乘以1000变成mW。以1MΩ档为例如果输出1V则光电流I1V/1MΩ1μA650nm处RA≈0.4A/W光功率1μA/0.4A/W2.5μW也就是0.0025mW。这正是代码里1MΩ档的系数。但是要注意这个响应度RA只在校准波长下成立。如果换成绿光520nm响应度大约会降到0.3左右同样光功率下输出电压会变小读数就会偏低。业余条件下如果测多种波长建议分别标定系数之后切档位时根据波长微调。4.3 校准的三种实用方法代码烧进去后如果不校准读数充其量就是“有光没光”的定性显示要说定量精度还差得远。最靠谱的校准方法我用过三种第一种用已知功率光源校准。找一台成品光功率计或者一个标称功率的激光笔把光同时照射到自制的光电二极管和被检的光功率计探头上然后调节校准系数让显示器读数和标准仪器一致。这个方法最准确但前提是你身边有标准仪器。第二种用恒流源模拟光电流校准。光电二极管输出其实就是电流源所以用精密电流源直接注入运放输入端给定一个已知电流然后看输出电压是否等于电流乘以反馈电阻。这个方法的好处是不依赖光源稳定性适合验证电路本身。业余条件下没有精密电流源可以用一个大阻值电阻串联干电池产生的微安级电流也够用。第三种用中性密度滤光片拼接标定。如果只有一个已知功率的基准点再用不同衰减倍数的滤光片组合出多个功率点做出多点标定表然后通过查表或拟合修正码片。这个方法可以提升线性度但需要额外的滤光片一般业余玩家不建议折腾。4.4 显示模块和串口调试的配合OLED显示做得好整个仪器才有“成品”的感觉。我用的SSD1306是I2C接口地址默认0x3CArduino的A4接SDA、A5接SCL。如果OLED不亮先检查地址很多克隆屏地址是0x3D在代码里改一下就好。调试时我习惯同时把数据打到串口监视器这样OLED只显示最终结果串口输出ADC原始值和计算后的功率。因为OLED刷新有延迟如果数据跳变串口能更容易看到细节。5. 常见问题与排查技巧实录5.1 问题速查表DIY过程中我遇到过不少怪问题列个表方便对号入座。现象可能原因排查与解决读数一直跳不稳定反馈电阻过大、环境光干扰、连接松动加屏蔽罩用手固定连接检查运放供电是否纹波过大没有光时读数不为零暗电流、运放偏置电流、光电二极管被漏光完全遮住光电二极管重新标零检查光电二极管是否裸露在环境光中有光时读数很小或没反应光电二极管接反、反馈电阻没接上、运放饱和测量运放2脚和3脚电压是否接近检查输出端电压范围量程切换后读数不对切换开关接触不良、档位和代码参数不匹配用万用表直接量反馈电阻值确认代码中的电阻值是否正确读数随气温明显变化运放偏置电流受温度影响换用输入偏置电流更小的运放或者把暗电流校准做成按键触发ADC显示满量程或者零运放输出超出ADC范围或负压未接量运放输出如果是双电源供电确认负电源是否正常5.2 暗电流标零的正确姿势暗电流这个概念看起来简单做起来有很多细节。我最初的光电二极管没有完全遮光就做了零点校准导致微弱光测量结果系统性偏差。后来专门做了一个黑色热缩管套在光电二极管外面只留一个进光窗口测量前先把窗口完全堵住按一次零点校准键软件记录下此时的读数偏移之后所有测量值都减去这个偏移。零点校准的间隔要看使用场景。如果测量间隔很短环境温度变化不大校准一次能用很久。但如果在阳光下暴晒过或者刚开机给电路预热运放的偏置电流变化明显最好每次开机都重新校准一次。5.3 高阻反馈电阻带来的稳定性问题10MΩ反馈电阻加上光电二极管结电容和运放输入电容跨阻放大器非常容易自激振荡。现象就是输出端叠加了一个高频波形ADC读到的数值乱跳看起来像噪声其实已经是振荡了。解决办法是在反馈电阻上并联一个小电容形成一阶低通相位裕度提上来。电容值从10pF试到22pF我最终选了15pF对应的截止频率大约在1kHz左右对缓慢变化的连续光测量完全够用。如果测调制光信号电容值就要减小否则高频成分会被衰减掉。这里有个权衡想要不振荡电容不能太小想要响应快电容不能太大。5.4 防静电和焊接技巧运放输入端是高阻抗节点MOS输入的运放特别怕静电。焊接TL081的时候电烙铁最好接地或者拔掉电源用余热焊接避免烙铁尖上的感应电压把栅极击穿。我报废过一片运放症状是输出一直恒定在某个值怎么调都不变很大概率就是静电损伤。焊接反馈电阻时也要缩短电烙铁接触时间高温会导致电阻值偏移。精密电阻的温漂系数一般在50ppm/℃左右如果焊接时间过长电阻冷却后阻值可能回不到原值这种误差在小信号测量里就会放大成明显的读数偏差。6. 一点扩展思路往真正仪器方向再走一步DIY光功率计做到这步已经能解决日常测量需求了。如果后面想继续玩有几个方向性价比很高。一是把量程切换做成自动的。通过单片机检测输出电压如果接近满量程就自动降一档如果小于一定阈值就自动升一档实现全自动增益控制。这样不用手动拨开关体验接近成品仪器。二是加入波长补偿。BPW34的响应度随波长变化很大如果要做多波长测量可以在软件里预设一组波长-响应度查表用按钮选择当前光源波长系统自动应用对应的响应度系数。三是加数据记录功能。用MicroSD模块记录连续测量的功率变化曲线观察激光器的稳定性或者LED的老化趋势非常有用。把采集到的数据导到电脑里再画图比OLED上那点反馈直观得多。四是做对数放大器扩展量程。普通跨阻放大器的线性量程就两三个数量级如果想做到从nW到mW的巨大量程可以考虑AD8307这类对数检波芯片输出跟光功率的对数成正比UI上显示dBm就很方便。我个人在实际制作过程中最大的体会是这种仪器级的小项目原理图看起来很简单真正卡住人的往往是接地、绝缘、屏蔽这些看不到的细节。只要把反馈回路的高阻抗节点保护好了后面的一切都会顺畅很多。最后再分享一个小技巧如果你手头的运放没有合适的补偿电容可以先在反馈电阻两端并联一只47pF电容测试确认不振荡之后再逐步减小很多运放手册里推荐的相位补偿值只是一个安全起点实际调试还得看具体的电路板布局。