/* --------------------------------------------------------------
Application: 06 - Rev1
Release Type: Healthcare Application -- Simulation of PPG Heart Rate Monitor with Preemption
Class: Real Time Systems - Sp 2026
Author: William Wright
AI Use: Help with debouncing implementation, theme refactoring
---------------------------------------------------------------*/
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "driver/adc.h"
#include "math.h"
#include "esp_rom_sys.h"
#define HEARTBEAT_LED_PIN GPIO_NUM_2 // Status LED that pulses to indicate system is alive
#define ALERT_LED_PIN GPIO_NUM_16 // Cardiac alert LED. Illuminates on abnormal heart rate
#define ALERT_RESET_BTN_PIN GPIO_NUM_4 // Button to acknowledge and clear an active cardiac alert
#define PPG_SENSOR_PIN GPIO_NUM_32 // GPIO pin connected to the PPG (photoplethysmography) sensor
#define PPG_ADC_CHANNEL ADC1_CHANNEL_4 // ADC channel for PPG sensor (mapped to GPIO32)
#define MAX_BPM_QUEUE_DEPTH 40 // Max number of BPM readings buffered in the queue
#define TACHYCARDIA_THRESHOLD 500 // BPM threshold above which a tachycardia alert is triggered
// Synchronization Primitives
SemaphoreHandle_t xAlertResetSem; // Signals the alert reset task when the button is pressed
SemaphoreHandle_t xAlertLEDMutex; // Protects shared access to the cardiac alert LED state
SemaphoreHandle_t xConsoleMutex; // Protects access to serial console output across tasks
QueueHandle_t xPPGReadingQueue; // Passes BPM readings from the sensor task to the logger task
// Disables the interrupt and signals the alert reset task via semaphore
void IRAM_ATTR alert_reset_btn_isr_handler(void *arg) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// Disable interrupt immediately to suppress contact bounce
gpio_intr_disable(ALERT_RESET_BTN_PIN);
// Unblock the alert reset handler task
xSemaphoreGiveFromISR(xAlertResetSem, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// Runs continuously, blocking on the queue when no data is available.
void cardiac_data_logger_task(void *pv) {
float ppm_reading;
float readings_buffer[40];
int readings_index = 0;
while (1) {
float sum = 0.;
while (xQueueReceive(xPPGReadingQueue, &ppm_reading, portMAX_DELAY) == pdTRUE) {
readings_buffer[readings_index] = ppm_reading;
if (readings_index + 1 == 40) {
for (int i = 0; i < 40; i++) {
sum += readings_buffer[i];
}
float avg_reading = sum/40.;
sum = 0;
xSemaphoreTake(xConsoleMutex, portMAX_DELAY);
printf("Average PPM Reading: %.1f\n", avg_reading);
xSemaphoreGive(xConsoleMutex);
}
readings_index = (readings_index + 1) % 40;
}
}
}
// Waits for the alert reset button semaphore, debounces, then clears the cardiac alert LED
// Re-enables the button interrupt after handling so the next press can be detected.
void alert_reset_task(void *pvParameters) {
while (1) {
// Block until the button ISR fires
xSemaphoreTake(xAlertResetSem, portMAX_DELAY);
// Wait for button contact to stabilize (debounce delay)
vTaskDelay(pdMS_TO_TICKS(50));
// Clear the cardiac alert LED
xSemaphoreTake(xAlertLEDMutex, portMAX_DELAY);
gpio_set_level(ALERT_LED_PIN, false);
xSemaphoreGive(xAlertLEDMutex);
// Log the event
xSemaphoreTake(xConsoleMutex, portMAX_DELAY);
printf("Critical PPG reading cleared\n");
xSemaphoreGive(xConsoleMutex);
// Re-arm the button interrupt for the next alert
gpio_intr_enable(ALERT_RESET_BTN_PIN);
}
}
// Pulses the heartbeat status LED on a fixed interval to show the monitor is running
void heartbeat_led_task(void *pvParameters) {
bool led_state = false;
while (1) {
gpio_set_level(HEARTBEAT_LED_PIN, led_state);
led_state = !led_state;
vTaskDelay(pdMS_TO_TICKS(500));
}
vTaskDelete(NULL);
}
// Prints a periodic patient vitals status message to confirm the monitor is operating
void vitals_report_task(void *pvParameters) {
TickType_t currentTime = pdTICKS_TO_MS(xTaskGetTickCount());
TickType_t previousTime = 0;
while (1) {
previousTime = currentTime;
currentTime = pdTICKS_TO_MS(xTaskGetTickCount());
xSemaphoreTake(xConsoleMutex, portMAX_DELAY);
printf("Patient vitals nominal. Monitor uptime: %lu ms [interval: %lu ms]\n", currentTime, currentTime - previousTime);
xSemaphoreGive(xConsoleMutex);
vTaskDelay(pdMS_TO_TICKS(5000)); // Report every 5 seconds
}
vTaskDelete(NULL);
}
// Samples the PPG sensor at a fixed period and computes an estimated heart rate (BPM)
void sample_ppg_task(void *pvParameters) {
// Sensor calibration
const float GAMMA = 0.7;
const float RL10 = 50;
int raw = 0;
float v_measured = 0.0f; // Voltage at ADC input (V)
float r_measured = 0.0f; // Computed sensor resistance (Ohms)
float ppg_reading = 0.0f; // Reading from the photo sensor
const TickType_t samplePeriodTicks = pdMS_TO_TICKS(100);
TickType_t lastWakeTime = xTaskGetTickCount();
TickType_t currentTime = pdTICKS_TO_MS(xTaskGetTickCount());
TickType_t previousTime = 0;
bool was_above_threshold = false;
while (1) {
// Get timing information
previousTime = currentTime;
currentTime = pdTICKS_TO_MS(xTaskGetTickCount());
// Comment this out to avoid spam
// xSemaphoreTake(xConsoleMutex, portMAX_DELAY);
// printf("Sensor timing period: %lu ms\n", currentTime - previousTime);
// xSemaphoreGive(xConsoleMutex);
// Read raw ADC value from PPG sensor
raw = adc1_get_raw(PPG_ADC_CHANNEL);
// Convert raw ADC reading to estimated BPM via voltage → resistance → lux pipeline
// (In a real PPG system, this would be replaced with peak-detection on the waveform)
v_measured = (raw / 4096.0f) * 3.3f;
r_measured = (10000.0f * v_measured) / (3.3f - v_measured);
ppg_reading = pow(RL10 * 1e3 * pow(10, GAMMA) / r_measured, (1.0f / GAMMA));
// Push reading to logger queue
xQueueSend(xPPGReadingQueue, &ppg_reading, portMAX_DELAY);
// Trigger cardiac alert if reading exceeds tachycardia threshold
bool is_critical = ppg_reading > TACHYCARDIA_THRESHOLD;
if (is_critical && !was_above_threshold) {
xSemaphoreTake(xConsoleMutex, portMAX_DELAY);
printf("Critical PPG reading: %.1f\n", ppg_reading);
xSemaphoreGive(xConsoleMutex);
xSemaphoreTake(xAlertLEDMutex, portMAX_DELAY);
gpio_set_level(ALERT_LED_PIN, true);
xSemaphoreGive(xAlertLEDMutex);
}
was_above_threshold = is_critical;
// Comment this out to avoid spam
// xSemaphoreTake(xConsoleMutex, portMAX_DELAY);
// printf("Sensor task duration: %lu ms\n", pdTICKS_TO_MS(xTaskGetTickCount()) - currentTime);
// xSemaphoreGive(xConsoleMutex);
// Sleep until next sample window (precise period via DelayUntil)
vTaskDelayUntil(&lastWakeTime, samplePeriodTicks);
}
}
void app_main() {
// GPIO Initialization
gpio_reset_pin(HEARTBEAT_LED_PIN);
gpio_reset_pin(ALERT_LED_PIN);
gpio_set_direction(HEARTBEAT_LED_PIN, GPIO_MODE_OUTPUT);
gpio_set_direction(ALERT_LED_PIN, GPIO_MODE_OUTPUT);
gpio_install_isr_service(0);
gpio_reset_pin(ALERT_RESET_BTN_PIN);
gpio_set_direction(ALERT_RESET_BTN_PIN, GPIO_MODE_INPUT);
gpio_pullup_en(ALERT_RESET_BTN_PIN);
gpio_set_intr_type(ALERT_RESET_BTN_PIN, GPIO_INTR_NEGEDGE);
gpio_isr_handler_add(ALERT_RESET_BTN_PIN, alert_reset_btn_isr_handler, NULL);
// PPG sensor, configure as input via ADC
gpio_reset_pin(PPG_SENSOR_PIN);
gpio_set_direction(PPG_SENSOR_PIN, GPIO_MODE_INPUT);
adc1_config_width(ADC_WIDTH_BIT_12); // 12-bit ADC resolution
adc1_config_channel_atten(PPG_ADC_CHANNEL, ADC_ATTEN_DB_11); // 11dB attenuation for 0–3.3V range
// Synchronization primitives
xAlertResetSem = xSemaphoreCreateBinary();
xAlertLEDMutex = xSemaphoreCreateMutex();
xConsoleMutex = xSemaphoreCreateMutex();
xPPGReadingQueue = xQueueCreate(MAX_BPM_QUEUE_DEPTH, sizeof(float));
// Task creation
xTaskCreatePinnedToCore(sample_ppg_task, "PPG_SENSOR", 4096, NULL, 5, NULL, 1);
xTaskCreatePinnedToCore(alert_reset_task, "ALERT_RESET", 4096, NULL, 4, NULL, 1);
xTaskCreatePinnedToCore(cardiac_data_logger_task, "CARDIAC_LOGGER", 4096, NULL, 3, NULL, 1);
xTaskCreatePinnedToCore(vitals_report_task, "VITALS_REPORT", 4096, NULL, 2, NULL, 1);
xTaskCreatePinnedToCore(heartbeat_led_task, "HEARTBEAT_LED", 2048, NULL, 1, NULL, 1);
}