/*
STM32 Heart-rate linked light reminder
Target: Wokwi STM32F103C8T6 Blue Pill
Wiring:
- PA0: potentiometer signal, used as simulated BPM input
- PB6/PB7: SSD1306 I2C OLED SCL/SDA
- PA8: WS2812/NeoPixel data
- PB0: buzzer
- PB1: mute/reset button, active low
*/
#include <Adafruit_GFX.h>
#include <Adafruit_NeoPixel.h>
#include <Adafruit_SSD1306.h>
#include <Wire.h>
const int PULSE_ADC_PIN = PA0;
const int LED_STRIP_PIN = PA8;
const int BUZZER_PIN = PB0;
const int MUTE_BUTTON_PIN = PB1;
const int I2C_SCL_PIN = PB6;
const int I2C_SDA_PIN = PB7;
const uint8_t OLED_WIDTH = 128;
const uint8_t OLED_HEIGHT = 64;
const uint8_t OLED_RESET = 255;
Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET);
const uint8_t LED_COUNT = 8;
Adafruit_NeoPixel strip(LED_COUNT, LED_STRIP_PIN, NEO_GRB + NEO_KHZ800);
const uint16_t ADC_MAX_VALUE = 4095;
const uint8_t FILTER_SIZE = 12;
uint16_t sampleBuffer[FILTER_SIZE];
uint8_t sampleIndex = 0;
uint8_t sampleCount = 0;
uint32_t sampleSum = 0;
enum HeartState {
STATE_LOW,
STATE_NORMAL,
STATE_WARN,
STATE_ALERT
};
HeartState heartState = STATE_NORMAL;
int bpm = 75;
uint16_t filteredAdc = 0;
bool displayReady = false;
bool alarmMuted = false;
unsigned long lastSampleMs = 0;
unsigned long lastDisplayMs = 0;
unsigned long lastEffectMs = 0;
unsigned long lastSerialMs = 0;
bool lastButtonReading = HIGH;
bool stableButtonState = HIGH;
unsigned long lastButtonChangeMs = 0;
const unsigned long SAMPLE_INTERVAL_MS = 40;
const unsigned long DISPLAY_INTERVAL_MS = 250;
const unsigned long EFFECT_INTERVAL_MS = 30;
const unsigned long SERIAL_INTERVAL_MS = 500;
const int BPM_MIN = 45;
const int BPM_MAX = 150;
void setup() {
Serial.begin(115200);
delay(200);
pinMode(PULSE_ADC_PIN, INPUT_ANALOG);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(MUTE_BUTTON_PIN, INPUT_PULLUP);
noTone(BUZZER_PIN);
analogReadResolution(12);
Wire.setSCL(I2C_SCL_PIN);
Wire.setSDA(I2C_SDA_PIN);
Wire.begin();
displayReady = display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
if (!displayReady) {
Serial.println("SSD1306 init failed. Check PB6/PB7 and address 0x3C.");
} else {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println("STM32 Heart Link");
display.println("Starting...");
display.display();
}
strip.begin();
strip.clear();
strip.show();
for (uint8_t i = 0; i < FILTER_SIZE; i++) {
sampleBuffer[i] = 0;
}
Serial.println("STM32 heart-rate linked light reminder ready.");
Serial.println("Turn the potentiometer: 45-150 BPM. Press button to mute ALERT buzzer.");
}
void loop() {
unsigned long now = millis();
handleButton(now);
if (now - lastSampleMs >= SAMPLE_INTERVAL_MS) {
lastSampleMs = now;
sampleHeartInput();
updateHeartState();
}
if (now - lastEffectMs >= EFFECT_INTERVAL_MS) {
lastEffectMs = now;
updateLedEffect(now);
updateBuzzer(now);
}
if (now - lastDisplayMs >= DISPLAY_INTERVAL_MS) {
lastDisplayMs = now;
updateDisplay();
}
if (now - lastSerialMs >= SERIAL_INTERVAL_MS) {
lastSerialMs = now;
printStatus();
}
}
void sampleHeartInput() {
uint16_t raw = analogRead(PULSE_ADC_PIN);
if (sampleCount < FILTER_SIZE) {
sampleBuffer[sampleIndex] = raw;
sampleSum += raw;
sampleCount++;
} else {
sampleSum -= sampleBuffer[sampleIndex];
sampleBuffer[sampleIndex] = raw;
sampleSum += raw;
}
sampleIndex = (sampleIndex + 1) % FILTER_SIZE;
filteredAdc = sampleSum / sampleCount;
long mapped = map(filteredAdc, 0, ADC_MAX_VALUE, BPM_MIN, BPM_MAX);
bpm = constrain((int)mapped, BPM_MIN, BPM_MAX);
}
void updateHeartState() {
HeartState previous = heartState;
switch (heartState) {
case STATE_LOW:
if (bpm >= 65) {
heartState = STATE_NORMAL;
}
break;
case STATE_NORMAL:
if (bpm < 60) {
heartState = STATE_LOW;
} else if (bpm > 100) {
heartState = STATE_WARN;
}
break;
case STATE_WARN:
if (bpm < 95) {
heartState = STATE_NORMAL;
} else if (bpm > 120) {
heartState = STATE_ALERT;
}
break;
case STATE_ALERT:
if (bpm < 115) {
heartState = STATE_WARN;
}
break;
}
if (previous != heartState && heartState != STATE_ALERT) {
alarmMuted = false;
}
}
void handleButton(unsigned long now) {
bool reading = digitalRead(MUTE_BUTTON_PIN);
if (reading != lastButtonReading) {
lastButtonChangeMs = now;
lastButtonReading = reading;
}
if (now - lastButtonChangeMs > 35 && reading != stableButtonState) {
stableButtonState = reading;
if (stableButtonState == LOW) {
alarmMuted = !alarmMuted;
Serial.print("ALERT buzzer mute: ");
Serial.println(alarmMuted ? "ON" : "OFF");
}
}
}
void updateLedEffect(unsigned long now) {
switch (heartState) {
case STATE_LOW:
updateLowLed(now);
break;
case STATE_NORMAL:
setStripColor(0, 90, 0);
break;
case STATE_WARN:
updateWarnLed(now);
break;
case STATE_ALERT:
updateAlertLed(now);
break;
}
}
void updateLowLed(unsigned long now) {
bool on = (now % 1400) < 700;
if (on) {
setStripColor(0, 0, 100);
} else {
setStripColor(0, 0, 0);
}
}
void updateWarnLed(unsigned long now) {
uint16_t phase = now % 2000;
uint8_t level;
if (phase < 1000) {
level = map(phase, 0, 999, 25, 180);
} else {
level = map(phase, 1000, 1999, 180, 25);
}
setStripColor(level, level * 2 / 3, 0);
}
void updateAlertLed(unsigned long now) {
bool on = (now % 300) < 150;
if (on) {
setStripColor(180, 0, 0);
} else {
setStripColor(0, 0, 0);
}
}
void setStripColor(uint8_t red, uint8_t green, uint8_t blue) {
for (uint8_t i = 0; i < LED_COUNT; i++) {
strip.setPixelColor(i, strip.Color(red, green, blue));
}
strip.show();
}
void updateBuzzer(unsigned long now) {
if (heartState == STATE_ALERT && !alarmMuted) {
bool beepOn = (now % 1000) < 250;
if (beepOn) {
tone(BUZZER_PIN, 2200);
} else {
noTone(BUZZER_PIN);
}
} else {
noTone(BUZZER_PIN);
}
}
void updateDisplay() {
if (!displayReady) {
return;
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println("Heart Link Light");
display.drawLine(0, 10, 127, 10, SSD1306_WHITE);
display.setTextSize(2);
display.setCursor(0, 16);
display.print(bpm);
display.print(" BPM");
display.setTextSize(1);
display.setCursor(0, 38);
display.print("State: ");
display.print(stateName(heartState));
display.setCursor(0, 50);
display.print("LED: ");
display.print(ledText(heartState));
if (heartState == STATE_ALERT && alarmMuted) {
display.setCursor(98, 50);
display.print("MUTE");
}
drawBpmBar(0, 60, 128, 4);
display.display();
}
void drawBpmBar(int x, int y, int w, int h) {
int fillWidth = map(bpm, BPM_MIN, BPM_MAX, 0, w);
fillWidth = constrain(fillWidth, 0, w);
display.drawRect(x, y, w, h, SSD1306_WHITE);
display.fillRect(x, y, fillWidth, h, SSD1306_WHITE);
}
const char* stateName(HeartState state) {
switch (state) {
case STATE_LOW:
return "LOW";
case STATE_NORMAL:
return "NORMAL";
case STATE_WARN:
return "WARN";
case STATE_ALERT:
return "ALERT";
}
return "UNKNOWN";
}
const char* ledText(HeartState state) {
switch (state) {
case STATE_LOW:
return "Blue slow blink";
case STATE_NORMAL:
return "Green steady";
case STATE_WARN:
return "Yellow breathe";
case STATE_ALERT:
return "Red blink";
}
return "-";
}
void printStatus() {
Serial.print("ADC=");
Serial.print(filteredAdc);
Serial.print(" BPM=");
Serial.print(bpm);
Serial.print(" State=");
Serial.print(stateName(heartState));
Serial.print(" Muted=");
Serial.println(alarmMuted ? "YES" : "NO");
}Loading
stm32-bluepill
stm32-bluepill
Loading
ssd1306
ssd1306
旋钮模拟心率输入
OLED: BPM与状态
灯带: 蓝/绿/黄/红提醒
ALERT时蜂鸣
按键: 静音/恢复