↵本项目第一版本实现在arduino框架下通过MAX30102 对血氧和心率 进行实时监控,通过LM35 对温度进行监控 。所有数值在 ssd 1306 上进行显示。在血氧低过一定数值的时,设备会通过蜂鸣器发出警报。
第二版本实现手机实时监控并做数据分析(后续更新)
第三版本实现远程监控(后续更新)
本文面向完全新手的arduino及无编程经验人员。让大家低成本的制作一台血氧仪实时监控自己和家人的健康状态。
PS:单本设备不能替代专业医疗血氧仪,仅作补充使用
器材:
LM 35 温度传感器
SSD 1306 OLED 显示器
MAX30102 心率传感器
杜邦线 公母 公公 母母
蜂鸣器 micro bit51
**arduino uno **
面包板
绝缘胶布
接线
LM35
arduinoLM355VVCCGNDGNDA1S
SSD1306
arduinoSSD13063V33V3GNDGNDA4SDAA5SCL
蜂鸣器
arduino蜂鸣器5VVCCGNDGNDA1S
**MAX30102 **
arduinoMAX301023V33V3GNDGNDA4SDAA5SCL
如 max30102,ssd1306 同时需要连接A4 时,可以先连接面包板再连接进Arduino A4。
按照上述接线完成后,需要用到软件arduino
Software | Arduino
选择你要的版本;
搜索并安装以下库,点击install 安装
将代码复制进项目里:
#include <MAX3010x.h>
#include "filters.h"
#include <Adafruit_GFX.h> //OLED libraries
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
//#include <MAX30105extra.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3D ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Sensor (adjust to your sensor type)
MAX30105 sensor;
//MAX30105extra particleSensor;
const auto kSamplingRate = sensor.SAMPLING_RATE_400SPS;
const float kSamplingFrequency = 400.0;
// Finger Detection Threshold and Cooldown
const unsigned long kFingerThreshold = 10000;
const unsigned int kFingerCooldownMs = 500;
// Edge Detection Threshold (decrease for MAX30100)
const float kEdgeThreshold = -2000.0;
// Filters
const float kLowPassCutoff = 5.0;
const float kHighPassCutoff = 0.5;
// Averaging
const bool kEnableAveraging = false;
const int kAveragingSamples = 5;
const int kSampleThreshold = 5;
// limitation of sop2
const int spo2limit =95;
void setup() {
Serial.begin(9600);
display.begin(SSD1306_SWITCHCAPVCC, 0x3C); //Start the OLED display
delay(3000);
tone(3,1000); //And tone the buzzer for a 100ms you can reduce it it will be better
delay(1000);
noTone(3);
display.display();
if(sensor.begin() && sensor.setSamplingRate(kSamplingRate)) {
Serial.println("Sensor initialized");
}
else {
Serial.println("Sensor not found");
while(1);
}
}
// Filter Instances
LowPassFilter low_pass_filter_red(kLowPassCutoff, kSamplingFrequency);
LowPassFilter low_pass_filter_ir(kLowPassCutoff, kSamplingFrequency);
HighPassFilter high_pass_filter(kHighPassCutoff, kSamplingFrequency);
Differentiator differentiator(kSamplingFrequency);
MovingAverageFilter<kAveragingSamples> averager_bpm;
MovingAverageFilter<kAveragingSamples> averager_r;
MovingAverageFilter<kAveragingSamples> averager_spo2;
// Statistic for pulse oximetry
MinMaxAvgStatistic stat_red;
MinMaxAvgStatistic stat_ir;
// R value to SpO2 calibration factors
// See https://www.maximintegrated.com/en/design/technical-documents/app-notes/6/6845.html
float kSpO2_A = 1.5958422;
float kSpO2_B = -34.6596622;
float kSpO2_C = 112.6898759;
// Timestamp of the last heartbeat
long last_heartbeat = 0;
// Timestamp for finger detection
long finger_timestamp = 0;
bool finger_detected = false;
// Last diff to detect zero crossing
float last_diff = NAN;
bool crossed = false;
long crossed_time = 0;
int lowsopcount =0;
void loop() {
unsigned int val; //定义变量val
unsigned int dat;//定义变量dat
val=analogRead(1);//将val设置为读取到的A0的数值
dat=(500 * val) /1024; //计算出当前温度数字dat
auto sample = sensor.readSample(1000);
float current_value_red = sample.red;
float current_value_ir = sample.ir;
// Detect Finger using raw sensor value
if(sample.red > kFingerThreshold) {
if(millis() - finger_timestamp > kFingerCooldownMs) {
finger_detected = true;
}
}
else {
// Reset values if the finger is removed
differentiator.reset();
averager_bpm.reset();
averager_r.reset();
averager_spo2.reset();
low_pass_filter_red.reset();
low_pass_filter_ir.reset();
high_pass_filter.reset();
stat_red.reset();
stat_ir.reset();
finger_detected = false;
finger_timestamp = millis();
}
if(finger_detected) {
current_value_red = low_pass_filter_red.process(current_value_red);
current_value_ir = low_pass_filter_ir.process(current_value_ir);
// Statistics for pulse oximetry
stat_red.process(current_value_red);
stat_ir.process(current_value_ir);
// Heart beat detection using value for red LED
float current_value = high_pass_filter.process(current_value_red);
float current_diff = differentiator.process(current_value);
// Valid values?
if(!isnan(current_diff) && !isnan(last_diff)) {
// Detect Heartbeat - Zero-Crossing
if(last_diff > 0 && current_diff < 0) {
crossed = true;
crossed_time = millis();
}
if(current_diff > 0) {
crossed = false;
}
// Detect Heartbeat - Falling Edge Threshold
if(crossed && current_diff < kEdgeThreshold) {
if(last_heartbeat != 0 && crossed_time - last_heartbeat > 300) {
// Show Results
int bpm = 60000/(crossed_time - last_heartbeat);
float rred = (stat_red.maximum()-stat_red.minimum())/stat_red.average();
float rir = (stat_ir.maximum()-stat_ir.minimum())/stat_ir.average();
float r = rred/rir;
float spo2 = kSpO2_A * r * r + kSpO2_B * r + kSpO2_C;
if(bpm > 50 && bpm < 250) {
// Average?
if(kEnableAveraging) {
int average_bpm = averager_bpm.process(bpm);
int average_r = averager_r.process(r);
int average_spo2 = averager_spo2.process(spo2);
// Show if enough samples have been collected
if(averager_bpm.count() >= kSampleThreshold) {
Serial.print("Time (ms): ");
Serial.println(millis());
Serial.print("Heart Rate (avg, bpm): ");
Serial.println(average_bpm);
Serial.print("R-Value (avg): ");
Serial.println(average_r);
Serial.print("SpO2 (avg, %): ");
Serial.println(average_spo2);
if( average_spo2 >100) average_spo2 = 100;
display.clearDisplay(); //Clear the display
display.setTextSize(2); //Near it display the average BPM you can display the BPM if you want
display.setTextColor(WHITE);
display.setCursor(15,0);
display.println("BPM");
display.setCursor(70,0);
display.println(bpm);
display.setCursor(15,18);
display.println("SpO2");
display.setCursor(70,18);
display.println((int)average_spo2);
display.setCursor(15,36);
display.println("TMP");
display.setCursor(70,36);
display.println((int)dat);
display.display();
if ((int)average_spo2 < spo2limit){
lowsopcount++;
if (lowsopcount >3) {
tone(3,1000); //And tone the buzzer for a 100ms you can reduce it it will be better
delay(1000);
noTone(3);
}
}
if((int)average_spo2 >spo2limit) lowsopcount = 0;
}
}
else {
Serial.print("Time (ms): ");
Serial.println(millis());
Serial.print("Heart Rate (current, bpm): ");
Serial.println(bpm);
Serial.print("R-Value (current): ");
Serial.println(r);
Serial.print("SpO2 (current, %): ");
Serial.println(spo2);
if( spo2 >100) spo2 = 100;
display.clearDisplay(); //Clear the display
display.setTextSize(2); //Near it display the average BPM you can display the BPM if you want
display.setTextColor(WHITE);
display.setCursor(15,0);
display.println("BPM");
display.setCursor(70,0);
display.println(bpm);
display.setCursor(15,18);
display.println("SpO2");
display.setCursor(70,18);
display.println((int)spo2);
display.setCursor(15,36);
display.println("TMP");
display.setCursor(70,36);
display.println((int)dat);
display.display();
if ((int)spo2 < spo2limit){
lowsopcount++;
if (lowsopcount >3) {
tone(3,1000); //And tone the buzzer for a 100ms you can reduce it it will be better
delay(1000);
noTone(3);
}
}
if((int)spo2 >spo2limit) lowsopcount = 0;
}
}
// Reset statistic
stat_red.reset();
stat_ir.reset();
}
crossed = false;
last_heartbeat = crossed_time;
}
}
last_diff = current_diff;
}
}
将arduino 插入电脑中
选择你所用的arduino uno板
选择你的Port
每台电脑的Port 口可能不一样,不影响代码导入
点击上传按钮将代码烧录进arduino uno里
显示upload success,显示器显示adafruit的图案(杨桃),蜂鸣器发出声音表示代码正常导入arduino中。
将下面的代码命名为filters.h 并放在一起
#ifndef FILTERS_H
#define FILTERS_H
/**
* @brief Statistic block for min/nax/avg
*/
class MinMaxAvgStatistic {
float min_;
float max_;
float sum_;
int count_;
public:
/**
* @brief Initialize the Statistic block
*/
MinMaxAvgStatistic() :
min_(NAN),
max_(NAN),
sum_(0),
count_(0){}
/**
* @brief Add value to the statistic
*/
void process(float value) {
min_ = min(min_, value);
max_ = max(max_, value);
sum_ += value;
count_++;
}
/**
* @brief Resets the stored values
*/
void reset() {
min_ = NAN;
max_ = NAN;
sum_ = 0;
count_ = 0;
}
/**
* @brief Get Minimum
* @return Minimum Value
*/
float minimum() const {
return min_;
}
/**
* @brief Get Maximum
* @return Maximum Value
*/
float maximum() const {
return max_;
}
/**
* @brief Get Average
* @return Average Value
*/
float average() const {
return sum_/count_;
}
};
/**
* @brief High Pass Filter
*/
class HighPassFilter {
const float kX;
const float kA0;
const float kA1;
const float kB1;
float last_filter_value_;
float last_raw_value_;
public:
/**
* @brief Initialize the High Pass Filter
* @param samples Number of samples until decay to 36.8 %
* @remark Sample number is an RC time-constant equivalent
*/
HighPassFilter(float samples) :
kX(exp(-1/samples)),
kA0((1+kX)/2),
kA1(-kA0),
kB1(kX),
last_filter_value_(NAN),
last_raw_value_(NAN){}
/**
* @brief Initialize the High Pass Filter
* @param cutoff Cutoff frequency
* @pram sampling_frequency Sampling frequency
*/
HighPassFilter(float cutoff, float sampling_frequency) :
HighPassFilter(sampling_frequency/(cutoff*2*PI)){}
/**
* @brief Applies the high pass filter
*/
float process(float value) {
if(isnan(last_filter_value_) || isnan(last_raw_value_)) {
last_filter_value_ = 0.0;
}
else {
last_filter_value_ =
kA0 * value
+ kA1 * last_raw_value_
+ kB1 * last_filter_value_;
}
last_raw_value_ = value;
return last_filter_value_;
}
/**
* @brief Resets the stored values
*/
void reset() {
last_raw_value_ = NAN;
last_filter_value_ = NAN;
}
};
/**
* @brief Low Pass Filter
*/
class LowPassFilter {
const float kX;
const float kA0;
const float kB1;
float last_value_;
public:
/**
* @brief Initialize the Low Pass Filter
* @param samples Number of samples until decay to 36.8 %
* @remark Sample number is an RC time-constant equivalent
*/
LowPassFilter(float samples) :
kX(exp(-1/samples)),
kA0(1-kX),
kB1(kX),
last_value_(NAN){}
/**
* @brief Initialize the Low Pass Filter
* @param cutoff Cutoff frequency
* @pram sampling_frequency Sampling frequency
*/
LowPassFilter(float cutoff, float sampling_frequency) :
LowPassFilter(sampling_frequency/(cutoff*2*PI)){}
/**
* @brief Applies the low pass filter
*/
float process(float value) {
if(isnan(last_value_)) {
last_value_ = value;
}
else {
last_value_ = kA0 * value + kB1 * last_value_;
}
return last_value_;
}
/**
* @brief Resets the stored values
*/
void reset() {
last_value_ = NAN;
}
};
/**
* @brief Differentiator
*/
class Differentiator {
const float kSamplingFrequency;
float last_value_;
public:
/**
* @brief Initializes the differentiator
*/
Differentiator(float sampling_frequency) :
kSamplingFrequency(sampling_frequency),
last_value_(NAN){}
/**
* @brief Applies the differentiator
*/
float process(float value) {
float diff = (value-last_value_)*kSamplingFrequency;
last_value_ = value;
return diff;
}
/**
* @brief Resets the stored values
*/
void reset() {
last_value_ = NAN;
}
};
/**
* @brief MovingAverageFilter
* @tparam buffer_size Number of samples to average over
*/
template<int kBufferSize> class MovingAverageFilter {
int index_;
int count_;
float values_[kBufferSize];
public:
/**
* @brief Initalize moving average filter
*/
MovingAverageFilter() :
index_(0),
count_(0){}
/**
* @brief Applies the moving average filter
*/
float process(float value) {
// Add value
values_[index_] = value;
// Increase index and count
index_ = (index_ + 1) % kBufferSize;
if(count_ < kBufferSize) {
count_++;
}
// Calculate sum
float sum = 0.0;
for(int i = 0; i < count_; i++) {
sum += values_[i];
}
// Calculate average
return sum/count_;
}
/**
* @brief Resets the stored values
*/
void reset() {
index_ = 0;
count_ = 0;
}
/**
* @brief Get number of samples
* @return Number of stored samples
*/
int count() const {
return count_;
}
};
#endif // FILTERS_H
备注:
Max30102可以在外围一圈包裹上绝缘胶布以提高其精准性
杜邦线也可以用绝缘胶布进行稳定
代码在upload 的时候可能会出错显示有的库未找到,再次upload就行。
版权归原作者 winddoll 所有, 如有侵权,请联系我们删除。