/*
Interview project for Lexington Medical
Prepared 2025-10-27
STM32 Arduino Core (stm32duino)
Specification:
1. With no use of the crosswalk button the traffic lights should cycle between
red/yellow/green per the following sequence:
13 seconds: Mass Ave Green, Beacon St Red
2 seconds: Mass Ave Yellow, Beacon St Red
13 seconds: Mass Ave Red, Beacon St Green
2 seconds: Mass Ave Red, Beacon St Yellow
2. If ever both traffic lights are red, pedestrian light can be green,
otherwise pedestrian light should be red.
3. If the crosswalk button is pressed then whichever traffic light is
currently green (or yellow) should transition if needed to the yellow
state and then after finishing the 2 second yellow state, both traffic
lights should be red for 20 seconds before resuming green on whichever
traffic light holds the longest elapsed time since last being green.
4. Once the crosswalk button has been pressed, further presses should be
queued until one full cycle has finished (meaning 20 seconds for the
pedestrian cycle and 60 seconds for one full traffic cycle).
For reference guides see https://docs.wokwi.com/
*/
/*************************************************************************//**
* @file wokwi-stm32-blue-pill.ino
*
* @brief Holds hardware configurations and definitions.
*****************************************************************************/
extern "C" {
#include "traffic_ctrl.h"
}
/*************************************************************************//**
* HARDWARE DEFINES AND CONSTANTS
*****************************************************************************/
// Stoplight pin mapping.
constexpr uint8_t PIN_MASS_AVE_RED = A0;
constexpr uint8_t PIN_MASS_AVE_YELLOW = A1;
constexpr uint8_t PIN_MASS_AVE_GREEN = A2;
constexpr uint8_t PIN_BEACON_ST_RED = A5;
constexpr uint8_t PIN_BEACON_ST_YELLOW = A6;
constexpr uint8_t PIN_BEACON_ST_GREEN = A7;
constexpr uint8_t PIN_PED_RED = A3;
constexpr uint8_t PIN_PED_GREEN = A4;
constexpr uint8_t PIN_BTN = PB0; // active-low, use pull-ups
constexpr uint8_t PIN_BUZZER = PB9;
// Segment display pin mapping.
constexpr uint8_t A_pin = PB6;
constexpr uint8_t B_pin = PB13;
constexpr uint8_t C_pin = PB14;
constexpr uint8_t D_pin = PB15;
constexpr uint8_t E_pin = PC14;
constexpr uint8_t F_pin = PB7;
constexpr uint8_t G_pin = PA11;
constexpr uint8_t DP_pin = PA12;
constexpr uint8_t CA1 = PA15;
constexpr uint8_t CA2 = PB4;
const uint8_t displaySegments[8] = { A_pin, B_pin, C_pin, D_pin, E_pin, F_pin, G_pin, DP_pin };
/*************************************************************************//**
* FUNCTION DEFINITIONS. THESE FUNCTIONS MUST STAY IN THIS FILE DUE TO HW DEPENDENCIES!
*****************************************************************************/
// One-time hardware initialization to be completed on startup.
// Function must stay in .ino file for hardware dependency reasons.
void setup()
{
// Stoplight initializations.
pinMode(PIN_MASS_AVE_RED, OUTPUT);
pinMode(PIN_MASS_AVE_YELLOW, OUTPUT);
pinMode(PIN_MASS_AVE_GREEN, OUTPUT);
pinMode(PIN_BEACON_ST_RED, OUTPUT);
pinMode(PIN_BEACON_ST_YELLOW, OUTPUT);
pinMode(PIN_BEACON_ST_GREEN, OUTPUT);
pinMode(PIN_PED_RED, OUTPUT);
pinMode(PIN_PED_GREEN, OUTPUT);
// Crosswalk button initializations.
pinMode(PIN_BTN, INPUT_PULLUP); // active-low
// Crosswalk buzzer initializations.
pinMode(PIN_BUZZER, OUTPUT);
// Crosswalk timer initializations.
pinMode(A_pin, OUTPUT);
pinMode(B_pin, OUTPUT);
pinMode(C_pin, OUTPUT);
pinMode(D_pin, OUTPUT);
pinMode(E_pin, OUTPUT);
pinMode(F_pin, OUTPUT);
pinMode(G_pin, OUTPUT);
pinMode(DP_pin, OUTPUT);
pinMode(CA1, OUTPUT);
pinMode(CA2, OUTPUT);
// Initial setting of the stoplights. Turn off all lights initially.
digitalWrite(PIN_MASS_AVE_RED, false);
digitalWrite(PIN_MASS_AVE_YELLOW, false);
digitalWrite(PIN_MASS_AVE_GREEN, false);
digitalWrite(PIN_BEACON_ST_RED, false);
digitalWrite(PIN_BEACON_ST_YELLOW, false);
digitalWrite(PIN_BEACON_ST_GREEN, false);
digitalWrite(PIN_PED_RED, false);
digitalWrite(PIN_PED_GREEN, false);
// Initial setting of the crosswalk button.
digitalWrite(PIN_BTN, true); // active-low
// Initial setting of the crosswalk buzzer.
digitalWrite(PIN_BUZZER, false);
// Initial setting of the crosswalk timer. Turn off all segments initially.
digitalWrite(A_pin, false);
digitalWrite(B_pin, false);
digitalWrite(C_pin, false);
digitalWrite(D_pin, false);
digitalWrite(E_pin, false);
digitalWrite(F_pin, false);
digitalWrite(G_pin, false);
digitalWrite(DP_pin, false);
digitalWrite(CA1, false);
digitalWrite(CA2, false);
}
// Our loop function. Calls forgroundTask() to repeatedly service our traffic controller routines.
void loop()
{
// Initialize our systems.
systemInit();
// Initializations complete. Enter our loop to service the system.
while(1)
{
foregroundTask();
}
}
// User function to call meaningful initialization functions.
void systemInit(void)
{
// Before we enter an infinite loop, intialize the stoplights and crosswalk.
stoplightInit();
crosswalkInit();
}
// Foreground task that will repeatedly service our traffic controller routines.
// In an interrupt driven system, this would pair well with a backgroundTask()...
void foregroundTask(void)
{
// Save our current stoplight state at the top of the loop to avoid multiple getCurrStoplightState() calls.
stoplightState currentStoplightState = getCurrStoplightState();
// Set the stoplights based on our current state.
// Stoplights must get enabled/disabled from this file due to hardware definitions above.
setStoplights(currentStoplightState);
// Enter stoplight state machine for transition to new state when necessary.
stoplightRealtimeTask();
// Do not accept new crosswalk requests while we're initializing OR while we're already servicing a request.
if (currentStoplightState != INITIALIZE && currentStoplightState != PEDEST_GREEN)
{
// Check for button press
setCrosswalkButtonPress(!digitalRead(PIN_BTN));
// Play a tone based on the result of the button press
crosswalkTone(buttonRealtimeTask());
}
// If we're letting pedestrians cross, enter our crosswalk routine.
if (currentStoplightState == PEDEST_GREEN)
{
crosswalkRealtimeTask();
}
// If not, display 00 on the timer.
else
{
displayDigitIndex(DISPLAY_LEFT_DIGIT, displayNumbers[0]);
displayDigitIndex(DISPLAY_RIGHT_DIGIT, displayNumbers[0]);
// Reset entry marker so the next entry restarts timing.
setCrosswalkLastState(DISPLAY_ENTER);
}
}
// Function to set the stoplights based on the corresponding state.
// Function must stay in .ino file for hardware dependency reasons.
void setStoplights(stoplightState illuminationState)
{
if (illuminationState == FAULT)
{
// If we've faulted, turn all green and yellow lights off.
digitalWrite(PIN_MASS_AVE_YELLOW, false);
digitalWrite(PIN_MASS_AVE_GREEN, false);
digitalWrite(PIN_BEACON_ST_YELLOW, false);
digitalWrite(PIN_BEACON_ST_GREEN, false);
digitalWrite(PIN_PED_GREEN, false);
// Flash the red lights infinitely. We need a power cycle to reinitialize the system to exit.
while(1)
{
digitalWrite(PIN_MASS_AVE_RED, true);
digitalWrite(PIN_BEACON_ST_RED, true);
digitalWrite(PIN_PED_RED, true);
delay(FAULT_BLINK_MS);
digitalWrite(PIN_MASS_AVE_RED, false);
digitalWrite(PIN_BEACON_ST_RED, false);
digitalWrite(PIN_PED_RED, false);
delay(FAULT_BLINK_MS);
}
}
else
{
digitalWrite(PIN_MASS_AVE_RED, stoplightArray[illuminationState][0]);
digitalWrite(PIN_MASS_AVE_YELLOW, stoplightArray[illuminationState][1]);
digitalWrite(PIN_MASS_AVE_GREEN, stoplightArray[illuminationState][2]);
digitalWrite(PIN_BEACON_ST_RED, stoplightArray[illuminationState][3]);
digitalWrite(PIN_BEACON_ST_YELLOW, stoplightArray[illuminationState][4]);
digitalWrite(PIN_BEACON_ST_GREEN, stoplightArray[illuminationState][5]);
digitalWrite(PIN_PED_RED, stoplightArray[illuminationState][6]);
digitalWrite(PIN_PED_GREEN, stoplightArray[illuminationState][7]);
}
}
// Play a buzzer tone based on the passed boolean argument. Essentially PWM the buzzer for a moment.
// Function must stay in .ino file for hardware dependency reasons.
void crosswalkTone(bool playTone)
{
// If the tone was requested, play it!
if (playTone == true)
{
// Loop for the specified duration.
for (int i = 0; i < BUZZER_DURATION_LOOPS; i++)
{
// Delay based on the specified pulse. Pitch can be changed with the pulse define.
delay(BUZZER_PULSE_MS);
digitalWrite(PIN_BUZZER, true);
delay(BUZZER_PULSE_MS);
digitalWrite(PIN_BUZZER, false);
}
}
}
// Write to the segment display according to the corresponding input's pattern.
// Function must stay in .ino file for hardware dependency reasons.
void writeSegments(uint8_t displayValue)
{
for (int i = 0; i < NUMBER_OF_SEGMENTS; i++)
{
// Binary AND the display value to find which segments need to be set true/false.
if ((displayValue >> i) & 0x01)
{
// Turn these segments on.
digitalWrite(displaySegments[i], true);
}
else
{
// Turn these segments off.
digitalWrite(displaySegments[i], false);
}
}
}
// Function to write selected digit on display
// Function must stay in .ino file for hardware dependency reasons.
void displayDigitIndex(uint8_t displayIndex, uint8_t value)
{
// Set both selection pins to known, low state
digitalWrite(CA1, LOW);
digitalWrite(CA2, LOW);
// Write the actual segments we want to display
writeSegments(value);
// Activate the requested digit
if (displayIndex == 0)
{
digitalWrite(CA1, HIGH);
}
else
{
digitalWrite(CA2, HIGH);
}
}
// Crosswalk routine, primarily to service the timer.
// Function must stay in .ino file for hardware dependency reasons.
void crosswalkRealtimeTask(void)
{
// Initialize variables used for capturing our display time.
unsigned long elapsedTime;
uint8_t remainingTime;
// When entering PEDEST_GREEN, capture the start time once. Use static variable to allow persistence across calls.
static unsigned long startTime = 0;
if (getCrosswalkLastState() == DISPLAY_ENTER)
{
// Use getTimeInState() function to maintain alignment with stoplight state machine.
startTime = getTimeInState();
}
// Capture current time. Use getTimeInState() function to maintain alignment with stoplight state machine.
unsigned long currentTime = getTimeInState();
// Check if we've exceeded our digit switching frequency between the left and right side of the display.
if ((currentTime - getCrosswalkLastDigit()) >= SWITCH_PERIOD_MS)
{
// If so, set our new digit and change the side of the display.
setCrosswalketLastDigit(currentTime);
if (getCrosswalkLastState() == DISPLAY_LEFT_DIGIT)
{
displayDigitIndex(DISPLAY_LEFT_DIGIT, getCrosswalkBuffer(DISPLAY_LEFT_DIGIT));
setCrosswalkLastState(DISPLAY_RIGHT_DIGIT);
}
else
{
displayDigitIndex(DISPLAY_RIGHT_DIGIT, getCrosswalkBuffer(DISPLAY_RIGHT_DIGIT));
setCrosswalkLastState(DISPLAY_LEFT_DIGIT);
}
}
// Perform the math to find out how much time we have left in the crosswalk.
elapsedTime = (currentTime - startTime) / MS_CONVERSION;
remainingTime = PED_TIME_SEC - elapsedTime;
// Check if time has expired. If so, just set remaining time to 0.
if (remainingTime < 0)
{
remainingTime = 0;
}
// Update digit buffers
// '/ 10' gives us our tenths digit. '% 10' gives us our ones digit.
setCrosswalkBuffer(DISPLAY_LEFT_DIGIT, displayNumbers[ remainingTime / 10 ]);
setCrosswalkBuffer(DISPLAY_RIGHT_DIGIT, displayNumbers[ remainingTime % 10 ]);
}