/**
* @file PWMSquareWaveVisualizer.ino
* @brief PWM signal generator and OLED square wave visualizer
*
* This program generates PWM signals with sequential duty cycles (25%, 50%, 75%, 100%)
* at 1-second intervals and displays them as square waves on an SSD1306 OLED display.
*/
#include <U8glib.h>
#include <avr/pgmspace.h>
/** @brief OLED display instance using I2C interface */
U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NONE);
/** @brief PWM output pin number */
const uint8_t PWM_PIN = 9;
/** @brief Analog input pin number (for reference only) */
const uint8_t ANALOG_PIN = A0;
/** @brief PWM update interval in milliseconds */
const uint16_t PWM_UPDATE_INTERVAL_MS = 1000;
/** @brief PWM duty cycle values (0-255) stored in program memory */
const uint8_t PROGMEM dutyCycles[] = {64, 128, 191, 255};
/** @brief Number of duty cycle steps in sequence */
const uint8_t DUTY_CYCLE_COUNT = 4;
/** @brief Current PWM value being output */
uint8_t pwmValue = 0;
/** @brief Flag to track if redraw is needed (only updated when value changes) */
bool needsRedraw = true;
// Graph display constants
/** @brief Left margin of graph area */
const uint8_t GRAPH_X = 5;
/** @brief Top margin of graph area */
const uint8_t GRAPH_Y = 10;
/** @brief Width of graph area in pixels */
const uint8_t GRAPH_WIDTH = 118;
/** @brief Height of graph area in pixels */
const uint8_t GRAPH_HEIGHT = 40;
/** @brief Width of one square wave period in pixels */
const uint8_t WAVE_PERIOD = 20;
/**
* @brief Checks if it's time to perform an update based on interval
*
* @param lastUpdate Timestamp of last update
* @param interval Time interval in milliseconds
* @return true if update is due, false otherwise
*/
bool isUpdateDue(unsigned long lastUpdate, unsigned long interval)
{
return (millis() - lastUpdate >= interval);
}
/**
* @brief Updates timestamp to current time
*
* @param lastUpdate Reference to timestamp variable to update
*/
void updateLastTime(unsigned long &lastUpdate)
{
lastUpdate = millis();
}
/**
* @brief Retrieves duty cycle value for specified index from program memory
*
* @param index Duty cycle index to retrieve
* @return uint8_t PWM value (0-255) for the specified index
*/
uint8_t getDutyCycle(uint8_t index)
{
// Get value from PROGMEM to save RAM
return pgm_read_byte(&dutyCycles[index % DUTY_CYCLE_COUNT]);
}
/**
* @brief Advances to next index with wrap-around
*
* @param index Current index
* @return uint8_t Next index value
*/
uint8_t advanceIndex(uint8_t index)
{
return (index + 1) % DUTY_CYCLE_COUNT;
}
/**
* @brief Sets PWM output on specified pin
*
* @param pin PWM-capable output pin
* @param value Duty cycle value (0-255)
*/
void outputPWM(uint8_t pin, uint8_t value)
{
analogWrite(pin, value);
}
/**
* @brief Updates PWM signal using non-blocking timing
*
* Controls the PWM signal sequence without using delays
*/
void updatePwmSignal()
{
static unsigned long lastUpdate = 0;
static uint8_t index = 0;
uint8_t newPwmValue;
if (isUpdateDue(lastUpdate, PWM_UPDATE_INTERVAL_MS))
{
newPwmValue = getDutyCycle(index);
// Only update if value changed
if (newPwmValue != pwmValue) {
pwmValue = newPwmValue;
outputPWM(PWM_PIN, pwmValue);
needsRedraw = true; // Mark for OLED update only when value changes
}
index = advanceIndex(index);
updateLastTime(lastUpdate);
}
}
/**
* @brief Draws coordinate axes for the square wave graph
*/
void drawGraphAxes()
{
// y-axis
u8g.drawLine(GRAPH_X, GRAPH_Y, GRAPH_X, GRAPH_Y + GRAPH_HEIGHT);
// x-axis
u8g.drawLine(GRAPH_X, GRAPH_Y + GRAPH_HEIGHT, GRAPH_X + GRAPH_WIDTH, GRAPH_Y + GRAPH_HEIGHT);
}
/**
* @brief Draws a square wave based on PWM duty cycle
*
* @param dutyCycle PWM duty cycle value (0-255)
*/
void drawSquareWave(uint8_t dutyCycle)
{
// Calculate high portion width based on duty cycle
uint8_t highWidth = map(dutyCycle, 0, 255, 0, WAVE_PERIOD);
uint8_t numPeriods = GRAPH_WIDTH / WAVE_PERIOD;
for (uint8_t i = 0; i < numPeriods; i++)
{
uint8_t startX = GRAPH_X + i * WAVE_PERIOD;
if (highWidth > 0)
{
// Rising edge
u8g.drawLine(startX, GRAPH_Y + GRAPH_HEIGHT, startX, GRAPH_Y);
// High level
u8g.drawLine(startX, GRAPH_Y, startX + highWidth, GRAPH_Y);
// Falling edge
u8g.drawLine(startX + highWidth, GRAPH_Y, startX + highWidth, GRAPH_Y + GRAPH_HEIGHT);
}
// Low level
u8g.drawLine(startX + highWidth, GRAPH_Y + GRAPH_HEIGHT, startX + WAVE_PERIOD, GRAPH_Y + GRAPH_HEIGHT);
}
}
/**
* @brief Renders PWM value as text on the OLED
*
* @param dutyCycle PWM duty cycle value (0-255)
*/
void drawPwmText(uint8_t dutyCycle)
{
char buf[16]; // Buffer for text
uint8_t percentage = map(dutyCycle, 0, 255, 0, 100);
u8g.setFont(u8g_font_6x10);
snprintf(buf, sizeof(buf), "PWM: %d (%d%%)", dutyCycle, percentage);
u8g.drawStr(GRAPH_X, GRAPH_Y - 2, buf);
}
/**
* @brief Updates the OLED display with square wave visualization
*
* Only redraws the screen when PWM value has changed to reduce MCU load
* and minimize I2C communication overhead
*/
void updateDisplay()
{
// Only redraw if needed (when PWM value changes)
if (needsRedraw) {
u8g.firstPage();
do {
drawGraphAxes();
drawSquareWave(pwmValue);
drawPwmText(pwmValue);
} while (u8g.nextPage());
needsRedraw = false; // Reset flag after redraw
}
}
/**
* @brief Arduino setup function - initializes pins and hardware
*/
void setup()
{
pinMode(PWM_PIN, OUTPUT);
pinMode(ANALOG_PIN, INPUT);
}
/**
* @brief Arduino main loop function
*/
void loop()
{
updatePwmSignal();
updateDisplay();
}