UltraV:便携式紫外线指数仪,守护阳光下的安全
组件与物料
| 组件 | 说明 | 数量 |
|---|---|---|
| Arduino Nano R3 | 微控制器核心 | 1 |
| Texas Instruments ML-8511 | 紫外线传感器 | 1 |
| 128x64 OLED 显示屏 | SSD1306 驱动 | 1 |
| MT3608 DC-DC 升压模块 | 将 3V 电池电压升至 9V 供 Vin 引脚 | 1 |
| CR2 (3V) 电池 | 800mAh 容量 | 1 |
| CR2 电池座 | 固定电池 | 1 |
项目简介
因皮肤问题无法暴晒,我利用本该在海滩的时间制作了这款紫外线强度仪——UltraV。
基于 Arduino Nano rev3,搭配 UV 传感器、DC/DC 升压模块和小型 OLED 显示屏。核心目标是便携,随时随地读取 UV 指数。
步骤 1:零件清单
- Arduino Nano rev.3
- ML8511 UV 传感器
- 128×64 OLED 显示屏 (SSD1306)
- MT3608 DC-DC 升压模块
- CR2 电池
- CR2 电池座
- 拨动开关
- 外壳
步骤 2:传感器详解

ML8511 (Lapis Semiconductors) 适合室内外紫外线强度采集。内置运放将光电流转换为电压输出,便于 ADC 采集。掉电模式待机电流仅 0.1µA,大幅延长续航。
特性:
- 对 UV-A 和 UV-B 敏感的光电二极管
- 内置运算放大器
- 模拟电压输出
- 低供电电流 (300µA typ.) 和极低待机电流 (0.1µA typ.)
- 微型陶瓷 QFN 封装 (4.0×3.7×0.73mm, 12-pin)

未找到合适的透紫外线保护材料(塑料、玻璃均衰减 UV),石英熔融二氧化硅玻璃虽佳但价格高昂,最终决定让传感器裸露在外。
步骤 3:操作方法

开机对准太阳持续数秒,保持与光线方向一致。屏幕左侧显示即时值(每 200ms 刷新),右侧显示本次会话最大值——即您需要的读数。左下角同步显示 WHO 标准分级:LOW、MODERATE、HIGH、VERY HIGH、EXTREME。
步骤 4:电池电压与读数校准
选用 CR2 电池(800mAh),整个夏季使用后电压仍 2.8V,表现满意。工作电流约 100mA,但单次测量仅几秒。电池标称 3V,通过升压模块升至 9V 接 Vin 引脚。
为在屏幕显示电池电压,使用模拟输入 A2 测量。Arduino 模拟输入测 0-5V 需校准:
- 用最终电池供电(勿用 USB),万用表测 Arduino 5V 引脚电压(作为 ADC 参考)
- 将实测值填入代码,如实测 5.023V:
voltage = ((long)sum / (long)NUM_SAMPLES * 5023) / 1024.0;
代码对 10 个样本取平均。
步骤 5:原理图与接线

步骤 6:软件设计
显示屏使用 U8g2lib,字体丰富、定位灵活。UV 传感器读数采用 Arduino 3.3V 基准引脚(精度 1%)作为 ADC 参考:将 3.3V 引脚接 A1,对比传感器读数(A0)与基准读数,消除供电电压波动影响(只要高于 3.4V):
int uvLevel = averageAnalogRead(UVOUT);
int refLevel = averageAnalogRead(REF_3V3);
float outputVoltage = 3.3 / refLevel * uvLevel;
完整代码下载: UltraV_OLED.ino.zip
步骤 7:外壳设计

手工开矩形显示窗口效果不佳,改用 CAD 设计外壳。为极致小巧,CR2 电池外置于背部(电池座粘贴固定)。
STL 文件下载: UltraV.stl.zip
步骤 8:未来改进方向
- 配合紫外线光谱仪在多种条件下实测 UVI(光谱仪极其昂贵)
- 同步记录 ML8511 输出与 Arduino 读数
- 编写算法关联 ML8511 输出与实时真实 UVI 值,覆盖多种大气条件
步骤 9:图片集






步骤 10:致谢
完整代码
UltraV (C/C++)
/* Fabio Marzocca @ 2018
ADC 完全依赖 VCC,USB 供电时可能 4.75V-5.25V,引入误差。
利用板载精准 3.3V 基准(1% 精度)校正:
对 3.3V 引脚 (A1) 与传感器 (A0) 同步 ADC,通过比值还原真实读数。 */
#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, SCL, SDA, U8X8_PIN_NONE);
#define FIRST_ROW_Y 16
#define FIRST_ROW_X 16
#define BOX_H 38
const int UVOUT = A0;
const int REF_3V3 = A1;
const int VBATT = A2;
float maxUV = 0;
void setup() {
pinMode(UVOUT, INPUT);
pinMode(REF_3V3, INPUT);
pinMode(VBATT, INPUT);
u8g2.begin();
}
void loop() {
u8g2.firstPage();
do {
int uvLevel = averageAnalogRead(UVOUT);
int refLevel = averageAnalogRead(REF_3V3);
float outputVoltage = 3.3 / refLevel * uvLevel;
float uvIntensity = mapfloat(outputVoltage, 0.99, 2.6, 0.0, 15.0);
readBattery();
if (maxUV < uvIntensity) maxUV = uvIntensity;
u8g2.drawFrame(0, FIRST_ROW_Y+1, 128, BOX_H);
u8g2.setFont(u8g2_font_logisoso18_tf);
u8g2.setCursor(10, 40); u8g2.print(uvIntensity); // 即时值
u8g2.setCursor(70, 40); u8g2.print(maxUV); // 最大值
u8g2.setFont(u8g2_font_u8glib_4_tf);
u8g2.setCursor(10, 52); u8g2.print(F("INSTANT"));
u8g2.setCursor(75, 52); u8g2.print(F("MAXIMUM"));
showUVCategory();
u8g2.setCursor(88, 64); u8g2.print(F("F.Marzocca"));
} while (u8g2.nextPage());
delay(200);
}
void showUVCategory() {
char strCat[12];
byte categ = (byte)(maxUV + 0.5); // 四舍五入
if (categ >= 0 && categ < 3) strcpy(strCat, "LOW");
else if (categ >= 3 && categ < 6) strcpy(strCat, "MODERATE");
else if (categ >= 6 && categ < 8) strcpy(strCat, "HIGH !");
else if (categ >= 8 && categ < 10) strcpy(strCat, "VERY HIGH!");
else if (categ >= 11) strcpy(strCat, "EXTREME!");
u8g2.setCursor(0, 64); u8g2.print(strCat);
}
int averageAnalogRead(int pinToRead) {
byte numberOfReadings = 16;
unsigned int runningValue = 0;
for (int x = 0; x < numberOfReadings; x++) runningValue += analogRead(pinToRead);
runningValue /= numberOfReadings;
return runningValue;
}
float mapfloat(float x, float in_min, float in_max, float out_min, float out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
void readBattery() {
long battery = readBattVcc();
long VccMin = 2300;
byte batteryBar;
int barStep = 140;
batteryBar = (battery - VccMin) / barStep;
u8g2.setFont(u8g2_font_open_iconic_embedded_2x_t);
u8g2.drawGlyph(1, FIRST_ROW_Y, 73); // 电池图标
u8g2.setFont(u8g2_font_open_iconic_play_1x_t);
for (byte i = 1; i <= batteryBar; i++) {
u8g2.drawGlyph(128 - 9*i, FIRST_ROW_Y - 4, 75); // 电量条
}
u8g2.setFont(u8g2_font_freedoomr10_tu);
u8g2.setCursor(25, FIRST_ROW_Y);
u8g2.print(float(battery)/1000, 3); // 电压显示
}
long readBattVcc() {
int sum = 0, sample_count = 0;
long voltage = 0;
#define NUM_SAMPLES 10
while (sample_count < NUM_SAMPLES) {
sum += analogRead(VBATT);
sample_count++;
delay(2);
}
// 使用校准后的 5020mV 作为参考电压
voltage = ((long)sum / (long)NUM_SAMPLES * 5020) / 1024.0;
return voltage; // 返回毫伏
}
制造工艺