Recap:
I'm building a smart solar monitoring system that uses three panels with a clean reference to eliminate weather effects and directly measure dust-induced losses in real time. One panel stays pristine as a baseline, and comparing the two under identical sky conditions gives a performance ratio that reveals soiling immediately. The goal is to use environmental sensors and edge AI to predict exactly when cleaning is needed, before efficiency drops enough to impact revenue. This beats fixed-schedule cleaning or waiting for output to degrade.Also this is complementary to my Master's thesis of a minute Shape memory Alloy based solar panel cleaning robot
Previous posts:
- SolarSense - Part 1 - Introduction, The POC Built and The Plan
- SolarSense - Part 2 - Can Arduino CAN?
- SolarSense - Part 3 - PCB Schematics Walkthrough
- SolarSense - Part 4 - CAN Protocol Deep Dive and Implementation
- SolarSense - Part 5 - Making an Live Dashboard using Lab View
- SolarSense - Part 6 - Reading the Heat at K Type Thermocouple using MAX31855.
Why UV?
The obvious answer you might think is "because UV degrades solar panels like so many other things." And yes, that is absolutely true. UV radiation is the primary driver of photovoltaic cell degradation over years of operation. The casing yellows, the anti-reflection coating breaks down, Tracking cumulative UV dose over time gives a solid predictor for long-term efficiency loss that has nothing to do with dust.
But for this project, That would not be the answer, UV degradation is established fact and there is no point in doing any research on it. But what is interesting is that Solar irradiance (energy per unit area) and UV index do not move in together. Clouds scatter and absorb visible light far more aggressively than they block UV — there are days where the sky looks completely overcast and the UV index is still punishing. The panels see almost no visible light but a significant UV dose. If my model only tracks panel output and misses UV, it cannot distinguish "sky is genuinely dark" from "sky is bright but the panel surface is blocked." UV gives the model context about what the actual sky condition is, independent of what the panels are producing.
The key insight: UV index is a proxy for the radiative environment that survives cloud cover in a way that visible irradiance does not. That independence is exactly what makes it useful as a model input. Now, I have to emphasize that the UV is not an authoritative indicator for passing clouds etc but it will be helpful.
What the GUVA Sensor Actually Does
The GUVA-S12SD is a GaN-based Schottky-type photodiode that is sensitive from roughly 240 nm to 370 nm. That range covers UV-B (280–315 nm) and most of UV-A, but the response peaks around 360 nm and falls to near zero before 400 nm — so visible light is well and truly rejected. The sensor is genuinely seeing UV, not just acting as a general light sensor with a blue filter.

What the chip itself produces is a current proportional to incident UV intensity. The datasheet specifies a responsivity of 0.14 A/W at 300 nm and a photocurrent of 26 nA per UV index unit under sunlight conditions. On the Adafruit breakout module, this current feeds into an op-amp circuit that amplifies it to a usable voltage level. Adafruit’s documentation states the conversion as: V_out = 4.3 × I_photodiode_in_µA
| Condition | Approximate V_out | UV Index |
|---|---|---|
| Dark / indoors | 0.05 – 0.15 V | 0 – 1 |
| Overcast outdoor | 0.15 – 0.40 V | 1 – 4 |
| Hazy sun | 0.40 – 0.80 V | 4 – 8 |
| Clear noon Kerala summer | 0.90 – 1.40 V | 9 – 14 |
My 0.3 V reading at test time is UV Index 3 exactly right for indoors near a window. The formula to compute UV index from the output voltage is simply dividing by 0.1 V, or in firmware:
/* UV index in tenths from GUVA output voltage. Per Adafruit: UV index = V_out / 0.1 V → tenths = uv_mv / 10 */ uint32_t uv_idx10 = uv_mv / 10U;
The Code
There are two pieces of setup required before any ADC conversion can be usable on the STM32L4. The first is the calibration call unlike older STM32 families, the L4’s ADC has an internal calibration routine that must be run once at startup before the first conversion. It writes offset correction values to the ADC’s internal registers and takes only a few microseconds. Without it, readings come out inaccurate. This goes right after MX_ADC1_Init():
HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED);
After that, reading the UV channel each telemetry tick is straightforward. The ADC is in scan mode with four ranks, which means all four channels are sequenced on every trigger — you cannot read just rank 1 in isolation. You have to drain all four values or the next conversion starts out of step.
The Conversion Rank in CubeMX

/* UV sensor (GUVA analog) on ADC1 rank 1 (ADC_CHANNEL_5 / PA0).
Scan group has 4 ranks; all must be drained on every conversion. */
if (o < (int)sizeof(tlm) - 16)
{
uint32_t uv_raw = 0;
if (HAL_ADC_Start(&hadc1) == HAL_OK)
{
if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK)
uv_raw = HAL_ADC_GetValue(&hadc1);
/* Drain ranks 2-4 (HX94C RH, HX94C TEMP, RAIN AO) */
for (int r = 1; r < 4; r++)
{
if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK)
(void)HAL_ADC_GetValue(&hadc1);
}
HAL_ADC_Stop(&hadc1);
}
uint32_t uv_mv = uv_raw * 3300U / 4095U;
o += snprintf(tlm + o, sizeof(tlm) - o, " uv_mv=%lu", (unsigned long)uv_mv);
}
The Readings
Just Like in the measurement of the temperature at the thermocouple, I do not have a calibrated UV source or measurement system. So I am going to just see if the multi-meter reading of signal output is same as what STM32 picks up.


Final Notes
One line of calibration code. That is the whole lesson here. HAL_ADCEx_Calibration_Start on the STM32L4 is not optional.
The UV sensor is live and reading sensibly. Next I want to run it through a full day outdoors and plot the curve rising from low values before sunrise, peaking around local solar noon, dropping back. If the curve shape looks right and correlates with what I would expect for the season and latitude, I will be satisfied that the sensor is working correctly.
After that, the environmental sensors BMP280 and the HX94C are next in the queue. The ADC scan group already has the HX94C analog channels wired up, it is just waiting for code similar to what I did here. The sensing stack is getting close to complete. I am yet to solder the BMP280. I am hesitant because deep down in my heart, It feels like pressure is not going to be an useful parameter.