Finger Pulsmåler

Fra HTX Arduino
Spring til navigation Spring til søgning
Billede af modul til Finger Pulsmåling

Formålet med dette modul er at man på relativ simpel vis kan måle pulsen på en person, blot ved at placere fingeren mellem lysdioden og fotodioden på modulet.

Princip-diagram for Finger Pulsmåling

LED1 er en infrarød Lysdiode, der forsynes med ca. 12 mA gennem R2. Det infrarøde lys sendes gennem fingeren der skal måles på, og afhængigt af blodgennemstrømningen modtager D1, der er en infrarød Photodieode, mere eller mindre infrarødt lys, alt efter hvor man er i pulsslaget.

På det oprindelige modul var LED1 vendt forkert, så den ikke lyste (kan kontrolleres med de fleste mobil-kameraer).

Desuden var R1 monteret med 330 ohm, hvilket gav en kraftigt reduceret følsomhed. Den er rettet til 100k ohm, hvilket har forbedret det noget.

Det viste print er et standard Keyes-modul, og selv med de angivne rettelser, så er følsomheden for lille til at det er til at detektere pulsen fornuftigt, og der er samtidigt for meget støj i signalet til at det er fornuftigt at arbejde med.

Derfor etableres et ekstra print, inden signalet tages ind i PIC'en.

Principskitse af Finger Pulsmåling

Print Finger Pulsmåling

Keyes modulet KY039 tilsluttes på SV1, hvorefter signalet filtreres og forstærkes gennem IC1A og IC1B, hvorefter det kan tages ud på SV2, der er tilsluttet en PIC-port med stikforbindelser som standard udviklingsboard. Ved at vælge med en jumper på SV3 eller SV4 kan man bestemme om der skal måles på portens pin_2 eller pin_4.

Board og Schematich ligger i en Fil:Heart board.zip.

Diagram over Finger Pulsmåling Filter

Diagrammet til forstærkning og filtrering til Finger Pulsmåling

Filteret forsynes med 5V fra PIC-boardet, og C1 på 100uF fungerer blot som en filtrering af forsyningsspændingen.

R1 og R2 danner et midtpunkt, som fungerer som en virtuel stel (det midtpunkt som signalerne svinger omkring). Dette midtpunkt er yderligere stabiliseret med C2 på 10 uF. De to modstande er valgt til samme størrelse (10 k ohm), så midtpunktet ligger på 2,5V. Som en ændring kunne man overveje af anvende 5,6 k ohm til R2, så spændingen blev til ca. 1,8V - denne ændring skyldes at operationsforstærkerens udgang ikke arbejder symmetrisk. Den kan trække næsten til 0V, men kan ikke gå højere end 3,5V, når den forsynes med 5V.

Finger sp del.PNG

Denne rettelse er ikke testet.

Finger Filter.jpg
Printet med filteret, hvor man kan se hvordan fingeren placeres i måle-printet

Forklaring til filter til Finger Pulsmåling

Hvert trin i forstærkningen har to 1. ordens filtre, således at den lave knækfrekvens laves af højpasfilteret R3 og C3, mens den høje knækfrekvens laves af lavpasfilteret R4 og C5. Samtidigt sættes pasbåndsforstærkningen til 10 gange ved R4/R3. De to knækfrekvenser bestemmes som følger:

Finger knk freq.PNG

For at gennemregne filtrenes virkning anvendes komplekse tal, og beregningerne er udført både i MathCAD og i Maple, hvor man kan hente beregningerne i denne ZIP-fil.

Filtrenes virkning er illustreret i de viste frekvenskurver, hvor den røde kurve er et enkelt filter, mens den blå stiplede kurve er de to filtre efter hinanden. Den man kan se på kurven er at centerfrekvensen ligger omkring 2,5 Hz og at der forstærkes ca. 50 gange i det punkt, men en ret interessant frekvens, nemlig 50 Hz bliver dæmpet 2 gange (forstærkning 0,5 gang), så den teoretiske ide med filtrene ser rigtig god ud.

Finger kurver.PNG

I den gennemregnede Maple fil er der anvendt lidt andre komponenter, hvor C3 er hævet til 2,2uF og R4 er hævet til 3,3M ohm, så det både giver mere forstærkning, men også lavere centerfrekvens, som det illustreres her:

Finger kurver2.PNG

Det har resulteret i en centerfrekvens på ca. 0,9 Hz og en forstærkning på knapt 300 gange i det punkt. Denne version er dog ikke testet, og vil formodentlig kræve flere målinger og måske små-justeringer i softwaren.

Målinger på kredsløbet

For at eftervise, at kredsløbet fungerer som forventet, og således at man har noget fornuftigt at lave softwaren efter, så er der foretaget en række målinger, som det illustreres i det følgende:

En god måling med fingerpuls modulet
En god måling med fingerpuls modulet

Den første måling viser en ganske god måling, hvor man fint kan detektere pulsslaget, og spændingen har en fornuftig størrelse med en puls på ca. 350 mV. Tiden mellem pulsslagene er her 952 ms, hvilket svarer til en puls på ca. 63.

En acceptabel måling med fingerpuls modulet
En acceptabel måling med fingerpuls modulet

Den næste måling viser en acceptabel måling, hvor spændingen er nede på 200 mV, men alligevel en acceptabel størrelse at måle på. Tiden er her nede på 930 ms, hvilket svarer til en puls på 65. I billedet er der vist flere pulsslag, ved at timebase er sat til 500 ms pr. tern.

En måling med fingerpuls modulet, uden fingeren i føleren
En måling med fingerpuls modulet, uden fingeren i føleren

Hvis man måler på signalet uden der er en finger i føleren, så kan man stadig risikere at måle noget, som det viste - her er der over 2 sekunder mellem de målinger som man også vil kunne registrere. I dette tilfælde vil de blive afvist af softwaren, da den kommer ud over den grænse der er sat på 2 sekunder pr. pulsslag (en puls på 30). Det vil sandsynligvis være tilfældigt hvilke signaler der kommer, når man ikke har noget i føleren.

En måling med fingerpuls modulet, taget mens man sætter fingeren ind i føleren
En måling med fingerpuls modulet, taget mens man sætter fingeren ind i føleren

Idet man sætter fingeren ind i føleren, så vil fingeren blokere for en del af det infrarøde lys, og det bliver et helt andet niveau man får ind. Det giver nogle voldsomme udsving i målingen, hvor man kan se skift på omkring 3 V i den periode. Man kan også se at perioden varer ca. 3 sekunder, men at der efter de store udsving kommer nogle fornuftige målinger - de to sidste toppe i billedet vil sikkert kunne anvendes til en måling.

Forklaring til måleprincippet

Ud fra den måde pulssignalerne ser ud på er der opstillet en metode til at måle tiden mellem to pulsslag. Metoden er baseret på beskrivelserne i følgende figur:

Forklaringer til målingen med fingerpuls modulet
Forklaringer til målingen med fingerpuls modulet

For at softwaren skal være uafhængig af hvilken DC spænding der er overlejret i signalet, så beregnes der løbende en middelspænding, som den er illustreret på figuren.

For at man ikke skal have en bestemt størrelse signal i pulsslaget, så registreres der også en maksimal spænding i pulsslaget, og ud fra den maksimale spænding og middelspændingen beregnes der et 75% level, som ligger 75% oppe ad pulsslaget. Dette level anvendes til bestemmelsen af starten for pulsslaget.

Da man som vist tidligere får nogle voldsomme spændinger idet man sætter fingeren ind i føleren, så vil den maksimale spænding kunne fejlregistreres, og derfor har softwaren en funktion der nulstiller den maksimale spænding, hvis der ikke er modtaget pulsslag i 2 sekunder (svarende til en puls på 30).

Det sidste der er illustreret i målingen er et ignorerings vindue, hvilket hænger sammen med den måde pulsen måles på. Når man kommer over 75% level, så starter der en ny tidsmåling, og denne tidsmåling skal gerne forløbe indtil man i det næste pulsslag kommer over 75% level. For ikke at få forstyrrelser ind, så er der tidsmæssigt lavet et ignoreringsvindue på 240 ms, hvor der ikke vil blive registreret en tid, således at puls-signalet forhåbentligt er kommet pænt under de 75% som det er illustreret i målingen. Dette ignoreringsvindue på 240 ms gør at man ikke kan måle en puls over 245.

Software modulet Heart.jal

Det modulet kan gøre er at lave en tidsmåling på mellem pulsslag, og beregne en aktuel puls ud fra denne tidsmåling, samt at lave et gennemsnit over 9 målinger, så man får en rimelig stabil aflæsning af pulsen.

Modulet anvender interrupt på Timer 0, så dette kan blokere for andre moduler.

Beregningen af pulsen sker ikke automatisk, så der skal kaldes en service-rutine for at se om der er kommet en ny aflæsning.

Anvendelse af Heart.jal

Når man vil anvende Heart modulet, så skal man include Heart.jal som følger:

   include Heart.jal

Hvis Heart.jal ligger i \lib inde i c:\jalpack så kan compileren finde modulet.

Heart anvender ADC_Holst.jal, så den skal også ligge i \lib.

JAL-filen ligger sammen med et eksempel inde i Fil:Heart jal.zip

Heart modulet er testet på følgende kombinationer:

PIC pin
PIC16F690 a2, a4

I den testede version er det nødvendigt at anvende PIC16F690, ikke så meget af hensyn til modulet, men fordi den anvender en del hukommelse til midlinger.

Interface fil til Heart

I Heart_def.jal angives hvilken kanal der måles på ved:

   heart_channel = 2

Anvendelsen af Heart

I det demo-eksempel der også ligger i zip-filen med modulet anvendes Heart sammen med ALCD modulet, for at kunne vise både den umiddelbare pulsmåling og den gennemsnitlige puls.

Koden ser ud som følger:

 pin_c7_direction = output

 include ALCD

 ALCD_clear_screen()         -- (to first line)

 include Heart

 forever loop
    pin_c7 = Heart_puls_mark             -- Indikerer pulsslaget 
    if heart_values_ready then
       ALCD_cursor_position(0,0)   -- Visning af displayet
       ALCD_write_char("P")
       ALCD_write_char("u")
       ALCD_write_char("l")
       ALCD_write_char("s")
       ALCD_Dec_4(Heart_puls)      -- Skriv den sidste beregning af puls
       ALCD_write_char(" ")
       ALCD_write_char("A")
       ALCD_write_char("v")
       ALCD_write_char("g")
       ALCD_Dec_4(Heart_puls_avg)  -- Skriv gennemsnittet af puls
    end if
 end loop

Det der vises på pin_C7 er en visning af toppen af pulsslaget.

Når man kalder heart_values_ready, så får man at vide om der er beregnet et nyt puls-tal, og et nyt gennemsnit. Det er faktisk i det kald at beregningen sker.

Hvis der er en ny beregning skrives de to tal ud sammen med noget tekst.

Forklaring af Modul Heart software

Som beskrevet, så er det en tidsmåling mellem hver pulsslag der danner grundlaget for beregningen af pulsen.

Tidsmålingen er etableret ved at opsætte et interrupt, der sker hvert 4. ms, hvor det er målet at fange forkanten af hver pulsslag, og så registrere hvor lang tid der er gået siden den sidste forkant.

Når der registreres en forkant, så aflæses tidsværdien, og den placeres i et array, så det er muligt at midle på pulsværdierne. Der sættes også en bit der indikerer at pulsværdien ligger klar.

Ved at man kalder en service-rutine, som tjekker på om der ligger en ny værdi klar, så kan man få foretaget beregningerne på pulsen, inden der kommer en ny værdi. Det er derfor vigtigt at denne service-rutine kaldes jævnligt (gerne hurtigere end hvert 100. ms), så der ikke sker en skrivning i interruptet, mens man læser værdierne til beregning (kan have uheldige konsekvenser).

Initialisering af softwaren

Initialiseringen kan i Pseudokode udtrykkes som følger:

   Læs hvilken kanal der skal måles på i Heart_def.jal
   Inkluder ADC_holst til AD-aflæsning 0 - 5V
   Gør timer 0 klar til at interrupte hvert 4. ms
   Opsæt grænserne til at fange et pulsslag
   Rens ad-snit-arrayet, og klargør den løbende sum
   Klargør arrayet til middelværdien af tidsmålingerne

De første dele sker blot ved includes.

Timer 0 sættes op ved følgende kode:

const byte heart_tmr_value = 6

INTCON_TMR0IE = true
INTCON_PEIE = false
INTCON_INTE = false
INTCON_RABIE = false
OPTION_REG_T0CS = false
OPTION_REG_PSA = false
OPTION_REG_PS = 0b0011  -- *16
TMR0 = heart_tmr_value  -- Count to 256 - 6 = 250 gives an interrupt every 4 ms

Intcon_gie = true

Styringen af de forskellige interrupts er her lavet ret hård, da alt andet bliver slået fra. Det burde egentlig ikke være nødvendigt, da de forskellige interrupts er slået fra som standard. Det kunne give problemer med at få andre moduler der anvender andre interrupts til at fungere.

Den resterende del af initialiseringen sker under opsætningen af variablerne, således at alle variabler hvis værdi indgår i målingen er sat til nogle værdier der er klar til måling, og arrayene er gjort klar.

Interrupt-Funktionen i Heart modulet

Som beskrevet, så er kernen i målingen af pulsen lavet i interrupt-funktionen.

Udtrykt som pseudokode, så laver interrupt-funktionen følgende:

   Hvis det er Timer 0 der interrupter så
      Sæt tiden op, så det passer med 4 ms
      Læs AD-værdien på pulsmåleren
      Beregn middelværdien over ca. 1 sekund
      Hvis AD-læsningen er større en max
         sæt max til den nye læsning
         beregn nivauet for start pulsmåling 3/4 mellem middel og max
      Tæl tiden en op
      Hvis AD-læsningen er over niveauet for start pulsmåling
         Hvis tiden er over 240 ms
            Registrer den nye tidsmåling og gem den i arrayet til gennemsnit
            Start en ny tidsmåling
            Indiker at der er en tidsmåling klar til beregning
         Marker til puls-visning at vi er i toppen af pulsen
      Hvis der ikke er kommet en måling inden for 2 sekunder
         Nulstil tidsmålingen
         Annuller målingerne til gennemsnitsberegning
         Sæt Max ned med 200
      Klargør til næste interrupt

Det der gør at proceduren bliver en interruptfunktion er procedurehovedet, der ser ud som følger:

procedure Heart_Timer_interrupt is pragma interrupt
   if Intcon_tmr0if then

Testen på Intcon_tmr0if er det der siger at det er timer 0 der har interruptet

Hvis det er timer 0 der har interruptet, så skrives timerværdien til 6, hvor den så tæller op til overflow mellem 255 og 0, altså 250 gange á 16 us, så det bliver præcist 4 ms inden næste interrupt. Dette gøres først, så afviklingstiden ikke forstyrrer tællingen.
Herefter læses hvad AD-værdien er på pulsmåleren:

      TMR0 = heart_tmr_value      -- count 250 to overflow - gives 4 ms
      Heart_ad_resultat = adc_read(heart_channel)   -- Read AD

I arrayet Heart_save lagres 16 AD-værdier, men for at strække måletiden, så er det kun for hver 64. ms at der gemmes en værdi i arrayet, således at gennemsnittet er beregnet hen over 1,024 sekund.
For ikke at skulle loope hele arrayet igennem ved beregningen af gennemsnittet, så er der skrevet 0 i hele arrayet fra starten, og summen er sat til 0. Hver gang der beregnes et ny snit, så gøres det ved at lægge den nye til summen, og tage den der bliver skal overskrives og trække fra summen, inden den nye skrives ind i arrayet. På den måde indeholder summen altid summen af hvad der står i arrayet. Ud fra summen beregnes den gennemsnitlige AD-værdi.

      Heart_arr_cnt = Heart_arr_cnt + 1
      if Heart_arr_cnt == 16 then     -- Every 16th AD is put into an
         Heart_arr_cnt = 0            -- array of 16 (just about 1 sec.)
         Heart_sum = Heart_sum + Heart_ad_resultat - Heart_save[Heart_ptr]
         Heart_save [Heart_ptr] = Heart_ad_resultat
         Heart_ptr = (Heart_ptr + 1) % 16
         Heart_snit = Heart_sum / 16  -- Calculates an average AD value
      end if

For at kunne håndtere forskelligt gennemsnit og forskellig max-værdi på pulsmålingen, så registreres den maksimale AD-værdi, og når der kommer en ny max-værdi, så beregnes også det niveau, hvor man fanger pulsmålingen

      if Heart_ad_resultat > Heart_max_ad then
         Heart_max_ad = Heart_ad_resultat     -- Catch the top of a peak
         Heart_level = (Heart_snit + Heart_max_ad*3) / 4
      end if

Tiden tælles op, og et skridt betyder 4 ms - det tages der højde for i beregningen af pulsen.
Hvis målingen er over det niveau hvor man registrerer (se evt. forklaringen på målingen tidligere), så gør man næsten ingenting, hvis tidsmålingen er under 60, svarende til 240 ms. Grunden til det er at vi kan fange forkanten på den måde - hvis målingen har forløbet i mere end 240 ms (svarende til en puls på 245), så vil vi acceptere det som et nyt pulsslag, ellers er det bare at vi er inden for den samme top stadigvæk.
Det vil altså sige at hvis vi kommer der ind, så skal vi registrere tiden, som er tiden siden sidste gang vi kom der ind. Denne tid placeres i et midlingsarray, hvor vi også tæller hvor mange vi har lagt ind i arrayet - grunden til dette forklares ved udregningen af gennemsnittet.
Herefter gør vi klar til den nye tidsmåling, og sætter et flag som indikerer at der er en ny måling klar.
Til slut i koden er der blot en bit der indikerer om vi er inde i toppen eller ej, så man kan lave en visning af at modulet modtager pulsslag.

      Heart_time = Heart_time + 1             -- Count the number of 4 ms blocks
      if Heart_ad_resultat > Heart_level then -- If value is over half of the peak
         if Heart_time > 60 then        -- And time is more than what is 250 in pulse
            Heart_time_cnt = Heart_time -- Capture the time
            Heart_time_arr[Heart_time_ptr] = Heart_time_cnt -- Place it in an averaging array
            Heart_time_ptr = (Heart_time_ptr + 1) % 10
            if (Heart_time_antal < 10) then   -- Take just values after a reset
               Heart_time_antal = Heart_time_antal + 1
            end if
            Heart_time = 0
            heart_values_present = true
         end if
         Heart_puls_mark = high   -- pulse marker for display
      else
         Heart_puls_mark = low
      end if

Idet man sætter fingeren ind i måleren, så kommer der nogle voldsomme udsving, og det vil få maksværdien til at stige voldsomt. Det kan gøre at vi ikke kan måle længere, så det skal også håndteres. Det gøres ved at teste på om målingen tager for lang tid (2 sekunder, svarende til en puls på 30). Hvis vi kommer over den grænse, så startes tidsmålingen forfra, og vigtigst af alt, så sættes den maksimale AD-værdi ned med 200, så der er en god chance for at den næste spids ligger over.
Samtidigt med dette så nulstilles antallet af det der er gem til midling. Det betyder at midlingen starter forfra, så der ikke ligger fejlmålinger i der snittet beregnes efter.

      if Heart_time > 500 then    -- If nopulse (below 30) then reset parameters
         Heart_time = 0
         Heart_time_ptr = 0
         Heart_time_antal = 0
         Heart_max_ad = Heart_max_ad - 200
      end if

Den sidste detalje er at der gøres klar til at der kan interruptes igen:

      Intcon_tmr0if = false       -- Prepare for new interrupt

Funktionsbeskrivelse

Hele tidsmålingen i heart-modulet skulle gerne arbejde i baggunden, så der skal ikke kaldes noget for at den del virker.

For at se om der er et nyt pulsslag klar, så kalder man blot heart_values_ready der vil returnere true, når der er et nyt pulsslag klar.

Når der er et nyt pulsslag klar, så ligger de nye beregnede værdier i 2 variabler:

Heart_puls angiver pulsen ud fra det sidst målte pulsslag.

Heart_puls_avg angiver middelværdien af mindst 3 pulsslag og maks 9 pulsslag, så der kommer en løbende midling, hvis der måles en stabil puls. Hvis der ikke er målinger nok, så er denne variabel 0.

Koden inde i funktionen Modul Heart

Funktionen Heart_values_Ready fungerer som følger:

 
function heart_values_ready return bit is
   if heart_values_present then
      heart_values_present = false          -- The pulse is made from 15000

Selve funktionshovedet angiver at der returneres en bit.

Det første der testes på er om interrupt-rutinen har indikeret at der er en ny værdi klar. Hvis der er det, så angives det at værdien er brugt, ved at sætte den false igen. Denne måde at kommunikere med interruptet på gør at denne servicerutine skal kaldes jævnligt (gerne for hver 100 ms eller hurtigere). Ved at bruge denne teknik, så sikres det at interruptet ikke er ved at skrive i nogle variabler mens man aflæser dem, hvilket kunne give forkerte værdier.

      heart_puls = 15000 / Heart_time_cnt   -- interrupt pr. minute

Hvis der er kommet en ny måling, så beregnes pulsen ud fra den registrerede tid.

Beregningen tager udgangspunkt i at der tælles 250 gange i sekundet og der er 60 sekunder på et minut, som der det pulsen måles efter. Derfor regnes pulsen ud fra 15000 = 60 * 250

      if Heart_time_antal > 3 then
         heart_puls_avg = 0                 -- Calculate the average of the times
         for (Heart_time_antal - 1) using Heart_n loop -- but skipping the first value
            heart_puls_avg = heart_puls_avg + Heart_time_arr[Heart_n + 1]
         end loop
         heart_puls_avg = heart_puls_avg / (Heart_time_antal - 1)
         heart_puls_avg = 15000 / heart_puls_avg
      else
         heart_puls_avg = 0
      end if

I interruptet placeres tiderne også i et array til midling.

Da det kan være svært at stole på de første pulser, når man lige har sat fingeren ind i måleren, så nulstiller interruptet hvor mange der skal midles hvis en måling er svigtet. Dette sker ved at der er gået for lang tid, og der startes blot på en ny måling. Derfor kan man ikke stole på den første måling der foretages.

Som det kan ses i koden oven over beregnes der først et gennemsnit, når der er foretaget mere end 3 målinger, og der gør koden det, at den første måling ikke tages med i gennemsnittet, for at man hurtigere skyder sig ind på den rigtige puls.

Hvis der ikke beregnes et gennemsnit, så sættes middelværdien til 0.

      return true
   end if
   return false
end function

Den sidste del af koden der vises herover er bare den del som angiver i returværdien om der er foretaget en ny beregning eller ej.


Moduler på Holstebro HTX
Tastaturer Displays AD-konvertering I/O-ekspander Serielt Interface Færdige Andre
RC-tast - AD-tast - M_tast ALCD - LCD ADC_holst - ADC
mcp3201 - mcp3208
input - output Seriel_holst - Serial hardware
Serial hw int cts - Serial software
Stepmotor - RFID
RGB - RF-link - Afstand
Humidity - Analog temp - Dig temp
Accelerometer
Rotary Encoder

Oversigt over Hardware Moduler på Holstebro HTX

Keyes-moduler på Holstebro HTX
Simple Digitale Input Switch modul - Reedrør - Hall sensor - Optisk Skift - Photo Gate - Vibration sensor - Vibration switch - Tilt sensor - Kviksølv kontakt - Linje følger
Digitalt kodede Input IR Modtager - Humidity -Digital Temperatur
5 benede Input Rotary Encoder -XY Joystick
Digitale 4 benede Input Magic Cup Light - LED 3-farve - RGB - RF-link - Afstand
Justerbare analoge/digitale Input Reed Magnetsensor - Temperatur Niveau - Metal detektor - Flamme - Hall Kontakt - Almindelig Mikrofon - Følsom Mikrofon
Simple digitale Output LED 2-farve - Aktiv Buzzer - Blink LED - IR LED - Laser - Relæ modul - Passiv Buzzer
Analoge input Analog Temperatur - LDR - Finger Pulsmåler - Lineær Magnetfelt