Theo dõi nhịp tim bằng vi điều khiển PIC và cảm biến xung
Nhịp tim là thông số quan trọng nhất trong việc theo dõi sức khỏe của bất kỳ ai. Trong kỷ nguyên hiện đại của các thiết bị đeo được, có rất nhiều thiết bị có thể đo nhịp tim, huyết áp, bước chân, lượng calo bị đốt cháy và rất nhiều thứ khác. Các thiết bị này có cảm biến xung bên trong chúng để cảm nhận tốc độ xung. Hôm nay, chúng tôi cũng sẽ sử dụng cảm biến xung với Vi điều khiển PIC để đếm nhịp tim mỗi phút, các giá trị này sẽ được hiển thị thêm trên màn hình LCD 16×2. Chúng tôi sẽ sử dụng vi điều khiển PIC PIC16F877A trong dự án này.
Linh kiện cần thiết
- Vi điều khiển PIC16F877A
- Crystal 20 Mhz
- Tụ 33pF x 2
- Điện trở 4,7k
- LCD nhân vật 16×2
- Cảm biến 10K để điều khiển độ tương phản của màn hình LCD
- Cảm biến xung SEN-11574
- Dây đeo Velcro
- Bộ chia nguồn 5V
- Breadboard và dây dẫn
Cảm biến xung SEN-11574
Để đo nhịp tim chúng ta cần một cảm biến xung. Ở đây chúng tôi đã chọn cảm biến xung SEN-11574. Chúng tôi đã sử dụng cảm biến này vì có các code mẫu được cung cấp từ nhà sản xuất, nhưng đó là code Arduino. Chúng tôi đã chuyển đổi code đó cho vi điều khiển PIC của chúng tôi.
Cảm biến thực sự nhỏ và hoàn hảo để đọc nhịp tim trên dái tai hoặc trên đầu ngón tay. Nó có đường kính 0,625 và dày 0,125 từ phía PCB tròn.
Cảm biến này cung cấp tín hiệu tương tự và cảm biến có thể được điều khiển với 3V hoặc 5V, mức tiêu thụ dòng điện của cảm biến là 4 mA, rất phù hợp cho các ứng dụng di động. Cảm biến đi kèm với ba dây với cáp hookup. Ngoài ra, cảm biến đi kèm với Dây đeo ngón tay Velcro để đeo nó trên đầu ngón tay.
Sơ đồ cảm biến xung cũng được cung cấp bởi nhà sản xuất và cũng có sẵn trên sparkfun.com.
Sơ đồ cảm biến bao gồm cảm biến nhịp tim quang học, mạch RC khử nhiễu hoặc bộ lọc, có thể được nhìn thấy trong sơ đồ. R2, C2, C1, C3 và bộ khuếch đại hoạt động MCP6001 được sử dụng cho đầu ra analog được khuếch đại đáng tin cậy.
Có một vài cảm biến khác để theo dõi Heart Beat nhưng cảm biến xung SEN-11574 được sử dụng rộng rãi trong các dự án Điện tử.
Sơ đồ mạch cho cảm biến xung giao tiếp với vi điều khiển PIC
Ở đây chúng tôi đã kết nối cảm biến xung qua chân 2 của bộ vi điều khiển . Khi cảm biến cung cấp dữ liệu tương tự, chúng ta cần chuyển đổi dữ liệu tương tự thành tín hiệu số bằng cách thực hiện các tính toán cần thiết.
Bộ tạo dao động Crystal 20Mhz được kết nối qua hai chân OSC của bộ vi điều khiển với hai tụ 33pF bằng gốm. Màn hình LCD được kết nối qua cổng RB của vi điều khiển.
Giải thích code
Code này có chút phức tạp cho người mới bắt đầu. Nhà sản xuất đã cung cấp code mẫu cho cảm biến SEN-11574, nhưng nó được viết cho nền tảng Arduino. Chúng tôi cần chuyển đổi tính toán cho vi mạch của chúng tôi, PIC16F877A. Code hoàn chỉnh được đưa ra ở cuối dự án này với Video demo.
Code của chúng tôi tương đối đơn giản và chúng tôi đã thực hiện các bước bằng cách sử dụng trường hợp chuyển đổi. Theo nhà sản xuất, chúng tôi cần lấy dữ liệu từ cảm biến sau mỗi 2 mili giây. Vì vậy, chúng tôi đã sử dụng một ngắt timer sẽ kích hoạt một hàm trong mỗi 2 mili giây.
Dòng code của chúng tôi trong câu lệnh switch sẽ diễn ra như thế này:
Trường hợp 1: Đọc ADC
Trường hợp 2: Tính nhịp tim và IBI
Trường hợp 3: Hiển thị nhịp tim và IBI trên LCD
Trường hợp 4: IDLE (Không làm gì)
Bên trong chức năng ngắt timer, chúng tôi thay đổi trạng thái của chương trình thành Trường hợp 1: Đọc ADC trên mỗi 2 mili giây.
Vì vậy, trong hàm main , chúng tôi đã xác định trạng thái chương trình và tất cả các trường hợp chuyển đổi .
void main() { system_init(); main_state = READ_ADC; while (1) { switch (main_state) { case READ_ADC: { adc_value = ADC_Read(0); // 0 is the channel number main_state = CALCULATE_HEART_BEAT; break; } case CALCULATE_HEART_BEAT: { calculate_heart_beat(adc_value); main_state = SHOW_HEART_BEAT; break; } case SHOW_HEART_BEAT: { if (QS == true) { // A Heartbeat Was Found // BPM and IBI have been Determined // Quantified Self "QS" true when Arduino finds a heartbeat QS = false; // reset the Quantified Self flag for next time // 0.9 used for getting better data. actually should not be used BPM = BPM * 0.9; IBI = IBI / 0.9; lcd_com(0x80); lcd_puts("BPM:- "); lcd_print_number(BPM); lcd_com(0xC0); lcd_puts("I.B.I:- "); lcd_print_number(IBI); } } main_state = IDLE; break; case IDLE: { break; } default: { } } } }
Chúng tôi đang sử dụng hai thiết bị ngoại vi phần cứng của PIC16F877A: Timer0 và ADC .
Trong tập tin timer0.c,
TMR0 = (uint8_t) (tmr0_mask & (256 - (((2 * _XTAL_FREQ) / (256 * 4)) / 1000)));
Tính toán này đang cung cấp ngắt timer 2 mili giây. Công thức tính là
// TimerCountMax - (((delay(ms) * Focs(hz)) / (PreScale_Val * 4)) / 1000)
Nếu chúng ta thấy hàm timer_isr , thì đó là-
void timer_isr () { main_state = READ_ADC; }
Trong hàm này, trạng thái chương trình được thay đổi thành READ_ADC sau mỗi 2ms.
Sau đó, hàm CALCULATE_HEART_BEAT được lấy từ code ví dụ Arduino.
void calculate_heart_beat(int adc_value) { Signal = adc_value; sampleCounter += 2; // keep track of the time in mS with this variable int N = sampleCounter - lastBeatTime; // monitor the time since the last beat to avoid noise // find the peak and trough of the pulse wave if (Signal < thresh && N > (IBI / 5)*3) { // avoid dichrotic noise by waiting 3/5 of last IBI if (Signal < T) { // T is the trough T = Signal; // keep track of lowest point in pulse wave } } …………. ………………………..
Code hoàn chỉnh được đưa ra dưới đây.
CODE:
// PIC16F877A Configuration Bit Settings // 'C' source line config statements // CONFIG #pragma config FOSC = HS // Oscillator Selection bits (HS oscillator) #pragma config WDTE = OFF // Watchdog Timer Enable bit (WDT disabled) #pragma config PWRTE = OFF // Power-up Timer Enable bit (PWRT disabled) #pragma config BOREN = ON // Brown-out Reset Enable bit (BOR enabled) #pragma config LVP = OFF // Low-Voltage (Single-Supply) In-Circuit Serial Programming Enable bit (RB3/PGM pin has PGM function; low-voltage programming enabled) #pragma config CPD = OFF // Data EEPROM Memory Code Protection bit (Data EEPROM code protection off) #pragma config WRT = OFF // Flash Program Memory Write Enable bits (Write protection off; all program memory may be written to by EECON control) #pragma config CP = OFF // Flash Program Memory Code Protection bit (Code protection off) #include <xc.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <stdbool.h> #include "supporing_cfile\lcd.h" #include "supporing_cfile\eusart1.h" #include "supporing_cfile\adc.h" #include "supporing_cfile\tmr0.h" /* Hardware related definition */ #define _XTAL_FREQ 200000000 //Crystal Frequency, used in delay /* Program Flow related definition */ #define READ_ADC 1 #define CALCULATE_HEART_BEAT 2 #define SHOW_HEART_BEAT 3 #define IDLE 0 #define DEFAULT -1 volatile int rate[10]; // array to hold last ten IBI values volatile unsigned long sampleCounter = 0; // used to determine pulse timing volatile unsigned long lastBeatTime = 0; // used to find IBI volatile int P = 512; // used to find peak in pulse wave, seeded volatile int T = 512; // used to find trough in pulse wave, seeded volatile int thresh = 530; // used to find instant moment of heart beat, seeded volatile int amp = 0; // used to hold amplitude of pulse waveform, seeded volatile bool firstBeat = true; // used to seed rate array so we startup with reasonable BPM volatile bool secondBeat = false; // used to seed rate array so we startup with reasonable BPM volatile int BPM; // int that holds raw Analog in 0. updated every 2mS volatile int Signal; // holds the incoming raw data volatile int IBI = 600; // int that holds the time interval between beats! Must be seeded! volatile bool Pulse = false; // "True" when User's live heartbeat is detected. "False" when not a "live beat". volatile bool QS = false; // becomes true when finds a beat. int main_state = -1; int adc_value = 0; int tune = 0; /* Other Specific definition */ void system_init(void); void calculate_heart_beat(int adc_value) { Signal = adc_value; sampleCounter += 2; // keep track of the time in mS with this variable int N = sampleCounter - lastBeatTime; // monitor the time since the last beat to avoid noise // find the peak and trough of the pulse wave if (Signal < thresh && N > (IBI / 5)*3) { // avoid dichrotic noise by waiting 3/5 of last IBI if (Signal < T) { // T is the trough T = Signal; // keep track of lowest point in pulse wave } } if (Signal > thresh && Signal > P) { // thresh condition helps avoid noise P = Signal; // P is the peak } // keep track of highest point in pulse wave // NOW IT'S TIME TO LOOK FOR THE HEART BEAT // signal surges up in value every time there is a pulse if (N > 250) { // avoid high frequency noise if ((Signal > thresh) && (Pulse == false) && (N > (IBI / 5)*3)) { Pulse = true; // set the Pulse flag when we think there is a pulse IBI = sampleCounter - lastBeatTime; // measure time between beats in mS lastBeatTime = sampleCounter; // keep track of time for next pulse if (secondBeat) { // if this is the second beat, if secondBeat == TRUE secondBeat = false; // clear secondBeat flag int i; for (i = 0; i <= 9; i++) { // seed the running total to get a realisitic BPM at startup rate[i] = IBI; } } if (firstBeat) { // if it's the first time we found a beat, if firstBeat == TRUE firstBeat = false; // clear firstBeat flag secondBeat = true; // set the second beat flag //pulse_tmr_handle = bsp_harmony_start_tmr_cb_periodic(PULSE_CHECK_TIME_INTERVAL, 0, pulse_read_cb); // enable interrupts again return; // IBI value is unreliable so discard it } // keep a running total of the last 10 IBI values uint16_t runningTotal = 0; // clear the runningTotal variable int i; for (i = 0; i <= 8; i++) { // shift data in the rate array rate[i] = rate[i + 1]; // and drop the oldest IBI value runningTotal += rate[i]; // add up the 9 oldest IBI values } rate[9] = IBI; // add the latest IBI to the rate array runningTotal += rate[9]; // add the latest IBI to runningTotal runningTotal /= 10; // average the last 10 IBI values BPM = 60000 / runningTotal; // how many beats can fit into a minute? that's BPM! QS = true; // set Quantified Self flag // QS FLAG IS NOT CLEARED INSIDE THIS ISR } } if (Signal < thresh && Pulse == true) { // when the values are going down, the beat is over Pulse = false; // reset the Pulse flag so we can do it again amp = P - T; // get amplitude of the pulse wave thresh = amp / 2 + T; // set thresh at 50% of the amplitude P = thresh; // reset these for next time T = thresh; } if (N > 2500) { // if 2.5 seconds go by without a beat thresh = 530; // set thresh default P = 512; // set P default T = 512; // set T default lastBeatTime = sampleCounter; // bring the lastBeatTime up to date firstBeat = true; // set these to avoid noise secondBeat = false; // when we get the heartbeat back } } void main() { system_init(); main_state = READ_ADC; while (1) { switch (main_state) { case READ_ADC: { adc_value = ADC_Read(0); main_state = CALCULATE_HEART_BEAT; break; } case CALCULATE_HEART_BEAT: { calculate_heart_beat(adc_value); main_state = SHOW_HEART_BEAT; break; } case SHOW_HEART_BEAT: { if (QS == true) { // A Heartbeat Was Found // BPM and IBI have been Determined // Quantified Self "QS" true when arduino finds a heartbeat QS = false; // reset the Quantified Self flag for next time // 0.9 used for getting better data. actually should not be used //BPM = BPM * 0.9; // IBI = IBI / 0.9; //IBI = IBI * 2; // tune = BPM / 2; //lcd_com(0x01); lcd_com(0x80); lcd_puts("BPM:- "); lcd_print_number(BPM); lcd_puts (" "); lcd_com(0xC0); lcd_puts("I.B.I:- "); lcd_print_number(IBI); lcd_puts (" "); } } main_state = IDLE; break; case IDLE: { break; } default: { } } } } /* This Function is for system initializations. */ void system_init(void){ TRISB = 0x00; lcd_init(); // This will initialize the lcd TMR0_Initialize(); TMR0_StartTimer(); INTERRUPT_GlobalInterruptEnable(); INTERRUPT_PeripheralInterruptEnable(); ADC_Init(); } /* * Custom timer callback function */ void timer_isr() { main_state = READ_ADC; } void interrupt INTERRUPT_InterruptManager (void) { // interrupt handler if(INTCONbits.TMR0IE == 1 && INTCONbits.TMR0IF == 1) { TMR0_ISR(); } }
VIDEO: