Voltmeter Clock Technical Documentation
Detailed technical documentation including code architecture, algorithms, and implementation details
Voltmeter Clock - Technical Documentation
Code Architecture
Program Flow
setup()
├── Initialize Serial (9600 baud)
├── Initialize I2C (RTC, BMP180)
├── Configure PWM pins (9, 5, 6)
├── Configure input pins (button, encoder)
├── Attach interrupts (encoder pins 2, 3)
├── Initialize BMP180 sensor
└── Initialize DHT22 sensor
loop() [Continuous]
├── Read button state
├── Detect button press/release
│ ├── Short press → Toggle display mode
│ └── Long press → Enter time adjustment
├── If adjusting time
│ └── adjustTime()
├── If not adjusting
│ ├── Read sensors (humidity, pressure)
│ ├── Update display based on mode
│ │ ├── Mode 0 → updateTimeDisplay()
│ │ └── Mode 1 → updatePTH()
│ └── Output serial data (if enabled)
├── Control LEDs based on mode
├── Refresh RTC data
└── Set encoder limits based on adjustment step
Core Algorithms
1. Time Display Calculation
Hour Calculation
hrVal = (rtc.hour() % 12 + rtc.minute() / 60.0) * 255.0 / 12
Explanation:
-
rtc.hour() % 12: Converts 24-hour to 12-hour format (0-11) -
+ rtc.minute() / 60.0: Adds fractional hour based on minutes -
* 255.0 / 12: Maps 0-12 range to 0-255 PWM range
Example: 3:30 PM
- Hour = 15 % 12 = 3
- Fraction = 30 / 60 = 0.5
- Total = 3.5 hours
- PWM = 3.5 * 255 / 12 = 74.375
Minute Calculation
minVal = (rtc.minute() + rtc.second() / 60.0) * 255.0 / 60
Explanation:
-
rtc.minute(): Current minute (0-59) -
+ rtc.second() / 60.0: Adds fractional minute based on seconds -
* 255.0 / 60: Maps 0-60 range to 0-255 PWM range
Example: 3:30:45
- Minute = 30
- Fraction = 45 / 60 = 0.75
- Total = 30.75 minutes
- PWM = 30.75 * 255 / 60 = 130.6875
Second Calculation with Quarter-Second Interpolation
secVal = map(rtc.second(), 0, 60, 0, 255) + quarterSecVal
Quarter-Second Logic:
void handleQuarterSec() {
dMillis = millis() - startMillis;
if (dMillis < 250) {
quarterSecVal = 0;
} else if (dMillis < 500) {
quarterSecVal = 4.25 * 1 / 4; // = 1.0625
} else if (dMillis < 750) {
quarterSecVal = 4.25 * 2 / 4; // = 2.125
} else if (dMillis < 1000) {
quarterSecVal = 4.25 * 3 / 4; // = 3.1875
}
}
Explanation:
- Divides each second into 4 quarters (250ms each)
- Adds incremental PWM value during each quarter
-
4.25represents the PWM increment per full second (255/60 ≈ 4.25) - Provides smooth analog movement between second ticks
Timeline Example (at second 30):
- 0-250ms: quarterSecVal = 0
- 250-500ms: quarterSecVal = 1.0625
- 500-750ms: quarterSecVal = 2.125
- 750-1000ms: quarterSecVal = 3.1875
- Next second: Reset to 0, new second value
2. PTH Display Calculation
Pressure Mapping
hrVal = map(pressure, minPressureVal, maxPressureVal, 0, 255);
hrVal = constrain(hrVal, 0, 255);
Range: 950-1030 hPa → 0-255 PWM
- 950 hPa = 0 PWM (minimum)
- 1030 hPa = 255 PWM (maximum)
- Linear interpolation for values in between
Temperature Mapping
minVal = map(dhtTemperature, minTempVal, maxTempVal, 0, 255);
minVal = constrain(minVal, 0, 255);
Range: 40-100°F → 0-255 PWM
- 40°F = 0 PWM (minimum)
- 100°F = 255 PWM (maximum)
- Linear interpolation for values in between
Humidity Mapping
secVal = map(humidity, minRHVal, maxRHVal, 0, 255);
secVal = constrain(secVal, 0, 255);
Range: 0-100% RH → 0-255 PWM
- 0% = 0 PWM (minimum)
- 100% = 255 PWM (maximum)
- Direct percentage to PWM conversion
3. Rotary Encoder Decoding
The code uses quadrature decoding with interrupt handlers on both encoder signals.
Encoder State Machine
Signal States:
- A and B can be HIGH or LOW
- Four possible states: (LOW,LOW), (LOW,HIGH), (HIGH,LOW), (HIGH,HIGH)
Direction Detection:
- Clockwise: A changes before B (or specific state transitions)
- Counter-clockwise: B changes before A (or opposite transitions)
Interrupt Handlers
updateEncoderA() - Triggered on pin A changes:
if (A != A_prev) { // A has changed
if (A == LOW) { // Falling edge
if (B == HIGH) counter++; // CW
else counter--; // CCW
} else { // Rising edge
if (B == LOW) counter++; // CW
else counter--; // CCW
}
}
updateEncoderB() - Triggered on pin B changes:
if (B != B_prev) { // B has changed
if (B == LOW) { // Falling edge
if (A == LOW) counter++; // CW
else counter--; // CCW
} else { // Rising edge
if (A == HIGH) counter++; // CW
else counter--; // CCW
}
}
Counter Constraints:
counter = constrain(counter, minCounter, maxCounter)- Limits based on current adjustment step
4. Button State Machine
State Detection
if (buttonState == LOW && lastButtonState == HIGH) {
// Button pressed (falling edge)
buttonPressStart = millis();
buttonHeld = true;
}
else if (buttonState == HIGH && lastButtonState == LOW) {
// Button released (rising edge)
unsigned long pressDuration = millis() - buttonPressStart;
if (pressDuration >= longPressThreshold) {
// Long press (>500ms)
isAdjustingTime = true;
} else {
// Short press (<500ms)
toggleDisplayMode();
}
}
Debouncing: Handled by checking state changes (edge detection)
5. Time Adjustment State Machine
Adjustment Steps
Step 0: Hour Adjustment
├── Counter range: 0-11
├── Display: Only hour voltmeter active
├── LED: Hour LED blinking
└── On button press → Step 1
Step 1: Minute Adjustment
├── Counter range: 0-59
├── Display: Only minute voltmeter active
├── LED: Minute LED blinking
└── On button press → Step 2
Step 2: Second Adjustment
├── Counter range: 0-59
├── Display: Only second voltmeter active
├── LED: Second LED blinking
└── On button press → Exit adjustment
RTC Setting
// Step 0: Set hour
rtc.set(0, 0, counter, 0, 0, 0, 0);
// Parameters: second, minute, hour, dayOfWeek, day, month, year
// Step 1: Set minute
rtc.set(0, counter, rtc.hour(), 0, 0, 0, 0);
// Step 2: Set second
rtc.set(counter, rtc.minute(), rtc.hour(), 0, 0, 0, 0);
Note: The RTC library uses a specific parameter order that may differ from standard time formats.
Interrupt System
Interrupt Configuration
attachInterrupt(digitalPinToInterrupt(pinA), updateEncoderA, CHANGE);
attachInterrupt(digitalPinToInterrupt(pinB), updateEncoderB, CHANGE);
Interrupt Type: CHANGE - Triggers on both rising and falling edges
Why Interrupts?:
- Encoder can change rapidly
- Interrupts ensure no encoder steps are missed
- Non-blocking - doesn’t delay main loop
Interrupt Safety:
- Uses
volatilevariables for shared state - Minimal processing in interrupt handlers
- Constrains counter values to prevent overflow
Sensor Reading
BMP180 (Pressure/Temperature)
Reading Process:
sensors_event_t event;
bmp.getEvent(&event);
if (event.pressure) {
pressure = event.pressure; // hPa
bmp.getTemperature(&temperature); // °C
}
Error Handling: Returns -1 if read fails
Update Frequency: Every loop iteration (when not adjusting time)
DHT22 (Humidity/Temperature)
Reading Process:
humidity = dht.readHumidity(); // % RH
dhtTemperature = dht.readTemperature(true); // °F (true = Fahrenheit)
Error Handling: Checks for NaN (Not a Number), returns -1 if invalid
Update Frequency: Every loop iteration (when not adjusting time)
Note: DHT22 requires ~2 seconds between readings (library handles this)
PWM Output
Arduino PWM Details
PWM Pins Used: 9, 5, 6
- Frequency: ~490 Hz (pins 5, 6) or ~980 Hz (pin 9) on most Arduino boards
- Resolution: 8-bit (0-255)
- Duty Cycle: 0 = 0%, 255 = 100%
Voltmeter Response
Assumption: Voltmeters are calibrated to show:
- 0 PWM (0V) = Minimum scale reading
- 255 PWM (5V) = Maximum scale reading
Smoothing: Quarter-second interpolation provides smooth analog movement
Timing and Performance
Loop Timing
- Typical loop time: < 10ms (without sensor delays)
- DHT22 delay: ~2 seconds between readings (handled by library)
- Serial output: Every 500ms (blink interval)
Non-Blocking Delays
LED Blinking:
if (currentMillis - previousMillisBlink >= blinkInterval) {
previousMillisBlink = currentMillis;
// Toggle LED
}
Serial Output:
if (currentMillis - previousMillisSerial >= blinkInterval) {
previousMillisSerial = currentMillis;
// Print data
}
Benefits:
- Doesn’t block main loop
- Allows responsive button/encoder handling
- Smooth display updates
Memory Usage
RAM (Estimated)
- Global variables: ~100 bytes
- Stack (function calls): ~50 bytes
- Libraries: ~500-1000 bytes
- Total: ~650-1150 bytes (well within Arduino Uno’s 2KB SRAM)
Flash (Estimated)
- Code: ~8-12 KB
- Libraries: ~15-20 KB
- Total: ~23-32 KB (within Arduino Uno’s 32KB flash)
Code Optimization Opportunities
Current Implementation
- Sensor reads: Every loop (may be excessive)
- RTC refresh: Every loop (may be excessive)
- Serial output: Conditional (good)
Potential Improvements
- Add sensor read intervals (e.g., read every 2 seconds)
- Cache RTC values (refresh every second, not every loop)
- Optimize encoder interrupts (reduce processing)
- Add EEPROM storage for display preferences
- Implement brightness control (placeholder exists)
Debugging Features
Debug Flag
bool debug_println = false; // Set to true to enable
Debug Output Locations
- Button press/release
- Display mode changes
- Time adjustment steps
- Encoder counter values
- Sensor readings
- PWM values
Serial Monitor Usage
- Set
debug_println = true - Open Serial Monitor (9600 baud)
- Observe real-time system state
Known Limitations
- Brightness Control: Placeholder exists but not implemented
- 24-hour Format: Only 12-hour format supported
- Date Display: Not implemented (only time)
- Sensor Calibration: No user calibration for sensor ranges
- EEPROM Storage: Settings not saved across power cycles
- Error Recovery: Limited error recovery for sensor failures
Extension Points
Easy Additions
- Brightness Control: Implement step 3 in adjustment mode
- 24-hour Format: Add toggle for 12/24 hour display
- Date Display: Add date mode (day, month, year)
- Alarm Function: Add time-based alarm
- Data Logging: Store sensor data over time
Advanced Features
- WiFi Connectivity: Add ESP8266 for remote monitoring
- OLED Display: Add secondary digital display
- SD Card Logging: Store historical sensor data
- Web Interface: Remote control and monitoring
- Multiple Time Zones: Display multiple time zones
Testing Recommendations
Unit Tests
- PWM Calculations: Verify time/PTH mapping formulas
- Encoder Decoding: Test all rotation directions
- Button Detection: Verify short/long press detection
- Sensor Reading: Test with known values
Integration Tests
- Full Time Adjustment: Complete adjustment cycle
- Mode Switching: Toggle between modes multiple times
- Sensor Display: Verify PTH readings match actual values
- Long-term Operation: Run for extended periods
Hardware Tests
- Voltmeter Calibration: Verify PWM to voltmeter response
- LED Indicators: Test all LED states
- Power Consumption: Measure current draw
- Temperature Stability: Test in various temperatures
Code Quality Notes
Strengths
- Clear variable naming
- Good code organization
- Comprehensive comments
- Modular function design
Areas for Improvement
- Some magic numbers could be constants
- Error handling could be more robust
- Some functions could be further modularized
- Add input validation for sensor ranges
Dependencies
Required Libraries
- uRTCLib - RTC communication
- Adafruit_Sensor - Sensor abstraction
- Adafruit_BMP085_U - BMP180 driver
- DHT - DHT22 driver
Library Installation
Install via Arduino Library Manager:
- Search for “uRTCLib”
- Search for “Adafruit BMP085”
- Search for “DHT sensor library”
Hardware Compatibility
Tested Platforms
- Arduino Uno (primary target)
- Arduino Nano (should work)
- Arduino Mega (should work)
Sensor Compatibility
- BMP180: Compatible with BMP085 (same library)
- DHT22: Compatible with DHT11 (with code modification)
- RTC: DS1307 (other RTCs may require library changes)
Version History
Current Version
- Time display with quarter-second interpolation
- PTH display mode
- Time adjustment via rotary encoder
- LED indicators
- Serial debugging support
Future Versions
- Brightness control
- 24-hour format option
- Date display mode
- EEPROM settings storage