Design of a Dual-Axis Solar Tracking System Based on STM32

#Smart Hardware#STM32

Abstract

Fixed photovoltaic panels cannot adjust their attitude as the sun moves, which limits power generation efficiency. This paper presents the design and implementation of a solar tracking system based on the STM32. Using the STM32F103C8T6 as the control core, four BH1750 digital light sensors are arranged in four quadrants on the back of the panel and sampled through a TCA9548A I2C multiplexer. The MCU computes pairwise sum-differences of the four readings to obtain two-axis errors, and a P control law drives a dual-axis gimbal built from two MG996R servos (azimuth + elevation) toward the light. A strong-light threshold with hysteresis separates direct light from ambient light; a deadband and per-cycle step limiting suppress jitter; exponential-approach output smoothing keeps servo motion continuous. Three measures — non-blocking UART transmission, sliced OLED refreshing, and frame-anchored timing — pin the 50Hz software PWM frame period to 20ms, eliminating the servo "lurching" problem. An on-chip RAM ring blackbox supports post-mortem analysis of intermittent faults. Tests show that alignment accuracy and dynamic response meet the design requirements with smooth, jitter-free motion.

Keywords: STM32; solar tracking; BH1750; dual-axis gimbal; P control; software PWM

1 Introduction

The conversion efficiency of photovoltaic generation is closely tied to the angle of incidence: output peaks when sunlight hits the panel normal and drops as it deviates. A fixed panel stays near its optimal tilt only briefly around noon, wasting most of the day. A dual-axis tracker continuously adjusts the panel attitude so its normal points at the sun, significantly increasing daily energy yield.

Traditional low-cost solutions place four photoresistors at the panel corners and judge light direction from ADC voltage differences. This approach has three inherent defects: photoresistors vary widely between units and drift with temperature, requiring per-unit calibration; single-shot ADC sampling is vulnerable to ambient light steps and supply noise, causing false triggers; and bang-bang control makes the motor start and stop abruptly, overshooting and jittering, which wears the mechanics over time.

This work replaces photoresistors with the BH1750 digital sensor (no calibration, good consistency, direct lux output), expands to four channels through an I2C multiplexer, and combines a hysteresis strong-light gate, deadband-limited P control and exponential-approach smoothing to achieve continuous, lurch-free dual-axis tracking on an STM32F103C8T6.

2 System Overview

2.1 Architecture

The system consists of sensing, decision, actuation and interaction:

  • Sensing: four BH1750 sensors are placed at four quadrants (top-left, top-right, bottom-left, bottom-right) on a carrier board behind the panel, with a cross-shaped shade (1525 mm tall) at the center so adjacent sensors see a stable illuminance difference for off-normal light — the physical basis of difference-based direction finding. All four sensors share address 0x23 and are accessed through channels SC0SC3/SD0~SD3 of a TCA9548A 8-channel I2C multiplexer.
  • Decision: the STM32F103C8T6 (72 MHz) reads all four illuminances every 100 ms, computes the two-axis errors and runs the P control law.
  • Actuation: two MG996R servos form a standard dual-axis gimbal — the azimuth axis (Z) rotates horizontally about the vertical axis, the elevation axis (Y) pitches about a horizontal axis, each independently settable over 0~180°.
  • Interaction: a 0.96" SSD1306 OLED shows the four readings and gimbal state in real time; four onboard keys support manual jogging and calibration; USART1 provides online tuning commands.

2.2 Component Selection

CategoryPartKey specsRole
MCUSTM32F103C8T672 MHz, 64K Flash / 20K RAMSampling, control, PWM generation
Light sensorBH1750 ×4I2C, 1~65535 lux, high-res modeQuadrant illuminance
I2C switchTCA9548A8 channels, ADDR=0x70Resolves 4× address conflict
ServoMG996R ×20~180°, metal gearsAzimuth / elevation drive
DisplaySSD1306 OLED 0.96"128×64, I2C 0x3CReadings and status
KeysTact switches ×4OnboardManual jog and combos

2.3 Pin Assignment

PeripheralPinsModeNotes
TCA9548A / BH1750PB14 / PB15Soft I2CSCL / SDA
OLEDPB8 / PB9Soft I2CSeparate bus, no interference
Azimuth servo (Z)PA12Push-pull, soft 50Hz PWMPower-on center 90°
Elevation servo (Y)PA11Push-pull, soft 50Hz PWMPower-on center 90°
Keys Key1~Key4PA3 / PB5 / PB6 / PB7Input pull-upZ− / Z+ / Y− / Y+
UARTUSART1 PA9 / PA10115200 bpsTuning commands and status

The pin mapping was corrected by key testing: Key2 pitches the gimbal up/down, so PA11 is elevation; Key4 rotates it horizontally, so PA12 is azimuth — the opposite of the original labeling, and the code has been fixed accordingly.

3 Hardware Design

3.1 Quadrant Sensor Head

The sensor head uses an integrated carrier-board form: the TCA9548A module and four BH1750 modules are soldered onto one perforated board rather than hanging on dupont wires. The gimbal is a moving platform; loose jumpers under vibration are the number-one cause of I2C frame drops, showing up as twitchy tracking and jumpy readings — soldering everything down cures it. The four board corners correspond to the four physical positions, and the kPosToCh[] mapping table pins the physical-to-channel correspondence (calibrated by occlusion tests: top-left=CH2, top-right=CH3, bottom-left=CH0, bottom-right=CH1), so PCB routing differences never affect the software logic.

3.2 Servo Drive

The MG996R servos are driven by software PWM: 50 Hz period, 0.5~2.5 ms pulse width, with microsecond delays from the DWT cycle counter. PA11/PA12 are push-pull outputs. The SERVO_PAN_INVERT / SERVO_TILT_INVERT macros accommodate different servo-arm orientations; if the closed loop runs the wrong way after changing a macro, flip the corresponding tracking sign macro.

3.3 Mounting and Orientation

The azimuth axis is a 180° servo covering only half a circle, so placement determines whether the system can track the sun all day. The core requirement: point the gimbal due south at Z=90° (power-on center). Then Z=0 faces east and Z=180 faces west; the sun's daily arc east → south → west fills exactly the 180° travel, leaving the dead zone in the northern sky where the sun never passes. For the elevation axis, install with Y=90° vertical (normal horizontal), leaving ~90° margin at both ends to cover the full solar elevation from sunrise to sunset. The base must be leveled — a tilted base makes the azimuth axis non-vertical, shifting all four readings together so the loop never finds equilibrium.

4 Software Design

4.1 Main Loop and Frame Beat

The main loop is a super-loop paced by a 20ms software servo frame: each frame stamps its start (Servo_FrameMark), and Servo_Frame at the frame end waits until "this frame's start + 20ms". As long as "frame work + pulse output" is under 20ms, the frame period stays strictly constant regardless of workload variation; an occasionally overrunning frame neither waits nor compensates (better one slow frame than a burst). Sensing and control run every 5 frames (100 ms); the other tasks (key scanning, OLED, blackbox) run every frame. The main program flow is shown in Figure 4-1.

Main program flowchart
Figure 4-1 Main program flowchart

4.2 Light Sampling and Filtering

At power-on each of the four channels is initialized once into continuous high-resolution mode; afterwards every cycle reads directly, avoiding the 180 ms blocking measurement wait — one pass over four channels takes only ~3.3ms, supporting the 100ms control period. The sampling and filtering flow is shown in Figure 4-2.

Light sampling and filtering flowchart
Figure 4-2 Light sampling and filtering flowchart

Raw readings go through bad-sample rejection and then a first-order low-pass (EMA coefficient 1/2): TCA9548A switching transients or flashlight reflections produce isolated spikes of several thousand lux; feeding those straight into the P controller drives consecutive full steps — visible as "lurching". A single jump beyond 1500 lux is treated as a bad sample and the previous filtered value is retained:

/* First-order low-pass + bad-sample rejection */
int diff = (int)lux[pos] - (int)lux_f[pos];
if (diff > 1500 || diff < -1500) {
    dbg_bad_sample++;              /* bad sample: keep last filtered value */
} else {
    lux_f[pos] = (uint16_t)(((uint32_t)lux_f[pos] + lux[pos] + 1u) >> 1);
}

4.3 Strong-Light Gate with Hysteresis

Ambient light (30~100 lux on an overcast day) and direct light (flashlight/sun, above 300 lux) differ by orders of magnitude. The system takes the maximum of the four readings as the gate metric: peak ≥ 250 lux enters tracking (TRK); peak < 150 lux (60% of the entry threshold) returns to hold (HLD, angles latched), and the hysteresis band prevents state flapping at borderline illuminance. The threshold is tunable at runtime via @T<lux>, with the exit threshold following at 60%.

4.4 Pairwise-Difference P Control

The quadrant readings are summed by row and by column, then differenced to yield the elevation and azimuth errors:

/* Raw differences (direction signs not yet applied) */
raw_tilt = lux[LT] + lux[RT] - lux[LB] - lux[RB];  /* top row - bottom row */
raw_pan  = lux[LT] + lux[LB] - lux[RT] - lux[RB];  /* left col - right col */

/* P step: 20 lux deadband, 3° per-cycle limit (max 30°/s) */
e_tilt = tilt_dir * raw_tilt;
if (e_tilt > TRACK_DEADBAND_LUX || e_tilt < -TRACK_DEADBAND_LUX) {
    int32_t step = e_tilt * TRACK_KP_NUM / (TRACK_KP_DEN * 10);
    if (step >  TRACK_STEP_LIMIT) step =  TRACK_STEP_LIMIT;
    if (step < -TRACK_STEP_LIMIT) step = -TRACK_STEP_LIMIT;
    Angle_SetTilt((int)tilt_target + (int)step);
}

The complete flow of gate checking, pairwise differencing and dead-band limited stepping is shown in Figure 4-3.

Closed-loop tracking control flowchart
Figure 4-3 Closed-loop tracking control flowchart

Control parameters: the proportional gain yields a 4° step per 100 lux of difference; the 20 lux deadband suppresses noise jitter; the 3° per-cycle (100ms) limit corresponds to a maximum slew of 30°/s. If any sensor errors out, both axes hold and the I2C error flag is set. Direction signs (light on the left column decreases Z; light on the top row increases Y) were locked by flashlight testing and can be flipped at runtime via key combos or @S<z>,<y>. A @C command triggers one-shot direction self-learning (probe ±10° per axis and compare improvement); in practice it misjudges under narrow field-of-view conditions and is disabled by default.

4.5 Output Smoothing

The P controller only writes target angles; the actual output approaches the target exponentially every 20ms PWM frame: one quarter of the remaining distance per frame, capped at 3° per frame, with the velocity itself limited to change by at most 1° per frame (acceleration limiting). Targets step at 10Hz while the servo micro-adjusts at every one of the 50 frames per second, so motion is continuous without stutter. Key jogging, serial angle setting and the re-center command all go through the same smoothing path, so no angle command from any source produces mechanical shock. The smoothing flow is shown in Figure 4-4.

Output smoothing flowchart
Figure 4-4 Output smoothing flowchart (Servo_Slew)

4.6 PWM Frame Timing Protection

The software PWM period equals the time of one main-loop iteration; any blocking causes dropped frames — and a servo that suddenly jumps after losing frames is "lurching". Segmented timing (tm_sensor_max_us etc.) identified three culprits: blocking UART transmission of one 150-byte line ≈13ms, a full OLED row refresh measured at 69.4ms per field, and 3.3ms for four sensor reads. Three fixes:

  1. Non-blocking UART: UART_Print now uses a ring buffer + TX-complete interrupt; the main loop never waits;
  2. Sliced OLED refresh: changed values are only marked dirty; each frame OLED_Service() writes at most one character (~4.3ms), with the status line throttled to 200ms;
  3. Frame-anchored catch-up: at frame end wait until "this frame's start + 20ms" (see 4.1).

Before/after comparison: frames exceeding 25ms in a 12-second run dropped from 273 to 0, and single-frame angle jumps stayed at zero. Diagnostic counters appended to the serial status line (fm=longest frame ovf=overrun frames jmp=jumps bad=bad samples) give instant attribution: jmp>0 means a firmware bug, growing ovf means a blocked main loop, and all zeros point to power or mechanical issues.

4.7 RAM Blackbox

Intermittent faults (e.g. "only Z moves yet Y follows") are hard to reproduce on the spot. The firmware packs each frame's key state into 20 bytes written to a RAM ring buffer (512 × 20B = 10KB): timestamp, actual/target angles of both axes, four lux readings, key states, mode bits and frame period; writes only on change, plus a 500ms heartbeat when idle. Later, an SWD script reads the whole buffer and automatically attributes each action (key-triggered / loop-driven / unattributed frames). The mechanism costs no serial bandwidth and never stretches the PWM frame; together with the diagnostic counters it forms a complete observability story.

4.8 Interaction and Serial Commands

The OLED shows the four physical-position readings in a 2×2 layout (WYSIWYG), with line 4 showing Z:xxx Y:xxx plus a status word (TRK tracking / CAL learning / HLD hold / MAN manual) or I2C ERR. Keys step 2° per press, auto-repeat every 100ms, accelerating to 5° per step after 1s; any key activity switches to manual mode, stopping the loop from fighting the user. Combos (held 1s): Key1+Key4 returns to auto and resets 90/90; Key1+Key2 flips the Z direction; Key3+Key4 flips the Y direction.

CommandFunctionCommandFunction
@Z<deg> / @Y<deg>Set azimuth / elevation alone@T<lux>Tune the strong-light threshold
@ZY<z>,<y>Set both axes at once@S<z>,<y>Set both tracking direction signs
@AReturn to auto (reset 90/90)@COne-shot direction self-learning
@BClear the blackbox——

5 System Testing

TestMethodResult
Four-channel samplingOcclude one sensor, watch readings vs OLED positionCorrect response, mapping verified
Strong-light gateShine / remove flashlightTRK at 250 lux, back to HLD below 150 lux, no borderline flapping
Tracking alignmentMove flashlight slowly around the gimbalContinuous following, rests in deadband when aligned
Output smoothing30° target stepContinuous motion without stutter, no overshoot
Frame periodSerial diagnostic counters, 10s observationfm≤20 ovf=0 jmp=0 (273 overruns per 12s before the fix)
Manual/auto switchJog with keys, then shine flashlightSwitches to MAN instantly; flashlight re-triggers TRK after release

The standalone test firmware links to the 0x08000000 reset vector and runs directly (unlike the main project's bootloader+app partition model); main() corrects VTOR on its first line before SysTick starts, avoiding a HardFault from fetching vectors in the erased app slot. Flashing uses mass-erase + program + verify, with entry address and vector table statically validated via readelf/objdump beforehand.

6 Conclusion

This paper implemented a dual-axis solar tracking system based on the STM32. Digital quadrant differencing replaces the traditional photoresistor scheme, eliminating calibration and unit variation. The three-layer mechanism — hysteresis strong-light gate, deadband-limited P control, and exponential-approach output smoothing — builds motion stability into the control structure rather than into parameter tuning. Frame-anchored timing together with sliced UART/OLED handling solves the timing determinism problem of software PWM platforms; the RAM blackbox and diagnostic counters provide reproducible observability for intermittent faults. The system meets its alignment and dynamic-response requirements with continuous, jitter-free motion, is directly applicable to small photovoltaic panels, and its "sense — difference — P control — smooth output" framework and frame-timing protection methods generalize to other gimbal-tracking scenarios.

References

[1] STMicroelectronics. STM32F103xx Reference Manual (RM0008)[Z]. 2021. [2] STMicroelectronics. STM32F1 Cortex-M3 Programming Manual (PM0056)[Z]. 2018. [3] ROHM Semiconductor. BH1750FVI Ambient Light Sensor IC Datasheet[Z]. 2011. [4] NXP Semiconductors. TCA9548A Low-Voltage 8-Channel I2C Switch Datasheet[Z]. 2015. [5] Hu Shousong. Principles of Automatic Control (7th ed.)[M]. Beijing: Science Press, 2019. [6] Liu Huoliang, Yang Sen. STM32 Library Development in Practice[M]. Beijing: China Machine Press, 2013. [7] Solving sensor integration issues in solar tracking applications. Solar Energy, 2020. [8] ARM Ltd. Cortex-M3 Embedded Development: DWT Cycle Counter Usage Note[Z]. ARM Application Note, 2015.