Librería Arduino para monitorización de la frecuencia cardíaca con oxímetro de pulso
Uno de los parámetros que se monitorizan en mi proyecto de gestión del sueño
es el pulso. Para medirlo desarrollé un dispositivo basado en el comportamiento de la hemoglobina y la oxihemoglobina frente a las diferentes longitudes de onda de la luz. Básicamente se trata de medir cuánta luz de determinado tipo es capaz de atravesar o es reflejada en una zona del cuerpo bien irrigada. La frecuencia con la que ocurre un ciclo completo de este fenómeno permite medir el pulso.En la fase de diseño y prueba del dispositivo para medir el pulso desarrollé algunos pequeños programas para ayudarme a verificar que el montaje era correcto. En primer lugar escribí el código de abajo, que iba tomando los valores medidos cada cierto tiempo (como mínimo cada y como máximo cada ) cuando variaban un mínimo entre uno y el anterior (el valor que corresponde con ) y los monitorizaba desde un ordenador con una aplicación en Python para poder analizarlos posteriormente.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
#define PIN_OXIMETRO 0 // Pin analógico 0
#define MEDIDA_MINIMA 10 // Cambio menor monitorizado
#define TIEMPO_MAXIMO_MEDIDA 200 // Cambio menor entre medidas (determina la resolución vertical)
#define TIEMPO_MINIMO_MEDIDA 100 // Milisegundos entre medidas (determina la resolución horizontal)
int lectura_anterior_oximetro=0;
int lectura_oximetro;
unsigned long cronometro_minimo=0;
unsigned long cronometro_maximo=0;
void setup()
{
Serial.begin(9600);
//pinMode(PIN_OXIMETRO,INPUT); // Ya es entrada por defecto
}
void loop()
{
if(millis()>cronometro_minimo)
{
lectura_oximetro=analogRead(PIN_OXIMETRO);
if(abs(lectura_oximetro–lectura_anterior_oximetro)>MEDIDA_MINIMA||millis()>cronometro_maximo)
{
cronometro_minimo=millis()+TIEMPO_MINIMO_MEDIDA;
cronometro_maximo=millis()+TIEMPO_MAXIMO_MEDIDA;
lectura_anterior_oximetro=lectura_oximetro;
Serial.println(String(millis(),DEC)+«,»+String(lectura_oximetro,DEC));
}
}
}
|
Una vez ajustados los valores (empezando en medidas muy densas) conseguí una colección de valores del oxímetro de pulso a lo largo del tiempo que podía representar gráficamente utilizando una hoja de cálculo, LibreOffice Calc de LibreOffice, en concreto.
Con los datos recabados, como representa la imagen de arriba, la siguiente operación era determinar si la densidad de valores permitía calcular de manera fiable pero «económica» (no muestrear más de los datos necesarios) el valor del pulso; como puede verse en el gráfico de abajo, las medidas tomadas parecían servir para obtener los resultados que es razonable esperar.
.
A continuación, con la información del muestreo de datos, quedaba elaborar un algoritmo que midiera la frecuencia del pulso. Ateniéndose a la gráfica que, por simplificar, se asume que representa un trazado similar al complejo QRS, lo más sencillo parece medir los tiempos entre las partes más destacadas, con valores mayores (que corresponde con la zona qRs de despolarización de los ventrículos) descartando la zona más plana y «ruidosa», más difícil de medir, por tanto. La solución adoptada, que corresponde con el código de prueba de abajo, funciona conforme al siguiente procedimiento:
-
Detectar la zona que se está midiendo en cada caso para atender sólo a los picos de valor qRs y desechar el valle. Para hacerlo se podrían medir valores superiores a cierta constante pero existe el riesgo de que un individuo y/o unas circunstancias que, aunque proporcionalmente, subieran o bajaran los valores. Para evitarlo, se considera un valor en la zona mayor al que supera en cierto coeficiente al valor medio. De esta forma, la medida se auto-calibra sensiblemente y se podría ajustar aún más afinando el coeficiente, que en mi caso he conseguido experimentalmente durante las pruebas.
Elegir para la medida los valores de la zona descendente (Rs) del pico qRs, lo más cercanos posible al máximo de la curva. Para saber que se abandona la zona ascendente basta con comprobar que un nuevo valor es menor que el anterior y verificar que no se ha encontrado todavía el valor buscado puesto que, en general, hay varios valores en la zona descendente de qRs dependiendo de la densidad de muestreo. Para cronometrar el pulso se almacena el valor del instante en el que se encontró el punto (los milisegundos devueltos por millis()) y se compara con el siguiente.
Para asegurarse de que el valor que se mide es el mayor de la zona descendente de la curva más alta se usa una variable booleana ( en este ejemplo y en la librería) que se activa al entrar en la zona ascendente de la curva mayor y se desactiva una vez encontrado el primer valor descendente, que es el cronometrado.
Como lo habitual es representar la duración del pulso como pulsaciones por minuto (ppm) se corrige el valor de tiempo entre pulsaciones obtenido calculando dividiendo el tiempo total de la representación (un minuto, 60000 milisegundos) entre el intervalo obtenido al restar los milisegundos actuales (del valor actual) entre los anteriormente cronometrados.
Para evitar medidas falsas (como el dispositivo midiendo en vacío, por ejemplo) se verifica que el resultado se encuentra entre unos valores máximos y mínimos antes de darlo por cierto. Aunque se considera como media que un valor normal para un adulto sano en reposo se encuentra entre 60 y 100 ppm, hay valores admisibles por debajo, es fácil encontrar 40 ppm en un atleta en reposo, hasta 200 ppm sometido a un ejercicio intenso y más de 100 ppm en adultos sedentarios en estados de excitación, precisamente un factor interesante para el proyecto de gestión del sueño que me lleva a desarrollar este dispositivo de medición del pulso. Por esto, es recomendable relajar mucho estos valores de manera que no se pierdan los extremos, que precisamente podrían mostrar aspectos relevantes.
El nuevo valor medio se calcula disminuyendo la relevancia de la media actual en función del número de valores muestreados y se añade el último valor, ponderado también con un coeficiente que lo reduce más cuanto más valores se hayan medido hasta el momento.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
#define PIN_OXIMETRO 0 // Pin analógico 0
#define TIEMPO_MINIMO_MEDIDA 20 // Milisegundos entre medidas (determina la resolución horizontal)
#define COEFICIENTE_PULSO 1.25 // Coeficiente que determina la zona de valores en la que medir el pulso
#define PULSO_MENOR 30 // Ignorar valores menores (Es infrecuente un pulso más lento aún en reposo)
#define PULSO_MAYOR 180 // Ignorar valores mayores (Es infrecuente un pulso mayor en reposo)
#define MINUTO 60000.0 // Milisegundos en un minuto
float velocidad_pulso;
float lectura_media_oximetro=511.5;
unsigned long valores_contados=0;
int lectura_anterior_oximetro=0;
int lectura_oximetro;
boolean medir_pulso=false;
unsigned long cronometro_pulso=0;
unsigned long cronometro_oximetro=0;
void setup()
{
Serial.begin(9600);
//pinMode(PIN_OXIMETRO,INPUT); // Ya es entrada por defecto
}
void loop()
{
if(millis()>cronometro_oximetro)
{
cronometro_oximetro=millis()+TIEMPO_MINIMO_MEDIDA;
lectura_oximetro=analogRead(PIN_OXIMETRO);
valores_contados++;
lectura_media_oximetro=lectura_media_oximetro*(float(valores_contados–1)/valores_contados);
lectura_media_oximetro+=lectura_oximetro*(1.0/valores_contados);
if(lectura_oximetro>lectura_media_oximetro*COEFICIENTE_PULSO)
{
if(lectura_anterior_oximetro<lectura_oximetro)
{
medir_pulso=true;
}
else
{
if(medir_pulso)
{
velocidad_pulso=MINUTO/float(millis()–cronometro_pulso);
medir_pulso=false;
if(velocidad_pulso>PULSO_MENOR&&velocidad_pulso<PULSO_MAYOR)
{
Serial.println(«Pulso «+String(velocidad_pulso,DEC));
}
cronometro_pulso=millis();
}
}
}
lectura_anterior_oximetro=lectura_oximetro;
}
}
|
Por último, utilizando el algoritmo descrito antes, desarrollé la librería para calcular el pulso detectando la presencia de la hemoglobina o la oxihemoglobina (según la longitud de onda de la luz usada) del código de abajo.
La librería espera que se llame periódicamente a la función de muestreo pulso en mi proyecto de gestión del sueño
. En cualquier caso, por las pruebas que he hecho, no parece ser necesario; ya sea por el dispositivo o por el comportamiento del para ir calculando el pulso, que puede consultarse con la función o con la función el pulso medio. Además de por ser un recurso limitado, descarté utilizar interrupciones por no necesitar valores inmediatos sino sostenidos en el tiempo para monitorizar el pulso, un muestreo a determinada frecuencia ofrece suficiente información y no se obtiene mucha más (relevante) por aumentarlo ni es posible disminuirla mucho sin perder datos relevantes para el cálculo; en las primeras versiones del código para monitorizar la lectura del oxímetro de pulso descubrí que no era necesario atenerse a un tiempo de medida máximo ya que, si se consideraban correctamente las variaciones de valores sucesivos, era muy cercano al mínimo.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
//pulso.h
#if defined(ARDUINO) && ARDUINO>=100
#include «Arduino.h»
#else
#include «WProgram.h»
#endif
#define PULSO_MINIMO 5 // Pulso mínimo. Un pulso menor se considera un error.
#define PULSO_MAXIMO 200 // Pulso máximo. Un pulso mayor se considera un error.
#define COEFICIENTE_PULSO 1.25 // Coeficiente que multiplica el pulso medio para determinar si el valor medido está en la zona que se cronometra. Puede usarse para calibra el dispositivo
#define OXIMETRIA_MEDIA 511.5 // Medida media inicial del pulso (1023/2) Puede usarse para calibrar el dispositivo
#define PULSO_MEDIO 80.0 // Pulso medio de un adulto en reposo (sólo para referencia)
class Pulso
{
private:
byte pin_oximetro; // Pin analógico al que se conecta el sensor de pulso
int lectura_oximetro; // Último valor medido en el sensor de pulso
int lectura_anterior_oximetro; // Penúltimo valor medido en el sensor de pulso para compararlo con el último y establecer la zona de valores en la que se encuentra
unsigned long numero_lecturas_oximetro; // Número de medidas del oxímetro tomadas. Usado para calcular la media
unsigned long numero_lecturas_pulso; // Número de medidas de pulso tomadas. Usado para calcular la media
float lectura_media_oximetro; // Medida media del sensor de pulso. Usado para saber si un valor se encuentra en la zona de medida (que se cronometra) del pulso
boolean medicion_de_pulso_activa; // Verdadero si se ha entrado de la zona del pulso (para no confundir si aún se encuentra en una zona ya medida)
float velocidad_pulso; // Último valor de pulso calculado
float ultima_velocidad_pulso; // Último valor de pulso correcto
float velocidad_media_pulso; // Media de los pulsos calculados durante la última sesión (creación del objeto Pulso)
unsigned long cronometro_pulso; // Tiempo entre medidas de pulso consecutivas
protected:
public:
Pulso(byte pin_oximetro_solicitado);
~Pulso();
void monitorizar_pulso(); // Se llama periódicamente (entre 10 y 50 ms) para calcular el pulso
byte ultimo_pulso(); // Devuelve el último valor correcto de pulso muestreado
byte pulso_medio(); // Devuelve el pulso medio de la sesión
};
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
//pulso.cpp
#include «pulso.h»
Pulso::Pulso(byte pin_oximetro_solicitado)
{
pin_oximetro=pin_oximetro_solicitado;
//pinMode(pin_oximetro,INPUT); // Ya es entrada por defecto
numero_lecturas_oximetro=0;
numero_lecturas_pulso=0;
lectura_media_oximetro=OXIMETRIA_MEDIA;
velocidad_media_pulso=PULSO_MEDIO;
medicion_de_pulso_activa=false;
lectura_anterior_oximetro=0;
ultima_velocidad_pulso=0; // Para indicar que es un valor incorrecto por el momento
cronometro_pulso=0;
}
Pulso::~Pulso()
{
}
byte Pulso::ultimo_pulso()
{
return round(ultima_velocidad_pulso);
}
byte Pulso::pulso_medio()
{
return round(velocidad_media_pulso);
}
void Pulso::monitorizar_pulso()
{
lectura_oximetro=analogRead(pin_oximetro);
numero_lecturas_oximetro++;
lectura_media_oximetro=lectura_media_oximetro*(float(numero_lecturas_oximetro–1)/numero_lecturas_oximetro); // Cambiar la representatividad de la parte actual de la media de pulso
lectura_media_oximetro+=lectura_oximetro*(1.0/numero_lecturas_oximetro); // Añadir la nueva lectura a la media
if(lectura_oximetro>lectura_media_oximetro*COEFICIENTE_PULSO)
{
if(lectura_anterior_oximetro<lectura_oximetro) // Medida de valores creciente
{
medicion_de_pulso_activa=true;
}
else
{
if(medicion_de_pulso_activa)
{
velocidad_pulso=60000.0/float(millis()–cronometro_pulso); // Cálculo de las pulsaciones por minuto (representación habitual del pulso)
medicion_de_pulso_activa=false; // Ya se ha medido el pulso, no medir hasta entrar en una nueva zona ascendente
if(velocidad_pulso>PULSO_MINIMO&&velocidad_pulso<PULSO_MAXIMO)
{
numero_lecturas_pulso++;
ultima_velocidad_pulso=velocidad_pulso;
velocidad_media_pulso=velocidad_media_pulso*(float(numero_lecturas_pulso–1)/numero_lecturas_pulso); // Calcular la representatividad de la media actual en la nueva media
velocidad_media_pulso+=velocidad_pulso*(1.0/numero_lecturas_pulso); // Añadir la nueva lectura a la media
}
cronometro_pulso=millis();
}
}
}
lectura_anterior_oximetro=lectura_oximetro;
}
|
En el siguiente programa de ejemplo se muestra cómo utilizar la librería anterior para medir el pulso con un oxímetro de pulso. Además de instanciar la clase se llama periódicamente a la monitorización del nivel de oxihemoglobina/hemoglobina y con una periodicidad menor se muestra el valor del pulso calculado y de la media.
Para asegurar que las medidas son relevantes se programa una espera antes de mostrar ningún valor. Como el valor puede ser incorrecto (por ejemplo si el usuario se retira el dispositivo) sólo se muestran valores si están dentro del rango de los considerados válidos.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
#define PIN_PULSOMETRO 0
#define TIEMPO_MONITORIZACION_PULSO 20
#define TIEMPO_PRESENTACION_PULSO 5000
#include «pulso.h»
Pulso pulso(PIN_PULSOMETRO);
unsigned long cronometro_monitorizacion_pulso;
unsigned long cronometro_presentacion_pulso;
byte velocidad_pulso;
byte velocidad_media_pulso;
void setup()
{
Serial.begin(9600);
cronometro_monitorizacion_pulso=0;
cronometro_presentacion_pulso=TIEMPO_PRESENTACION_PULSO*2;
}
void loop()
{
if(millis()>cronometro_monitorizacion_pulso)
{
cronometro_monitorizacion_pulso=millis()+TIEMPO_MONITORIZACION_PULSO;
pulso.monitorizar_pulso();
}
if(millis()>cronometro_presentacion_pulso)
{
cronometro_presentacion_pulso=millis()+TIEMPO_PRESENTACION_PULSO;
velocidad_pulso=pulso.ultimo_pulso();
velocidad_media_pulso=pulso.pulso_medio();
if(velocidad_pulso)
{
Serial.print(«Pulso «);
Serial.print(velocidad_pulso,DEC);
Serial.print(» | Pulso medio «);
Serial.println(velocidad_media_pulso,DEC);
}
}
}
|