//////////////////////////////////// /* Author: Robert Crews main/setup thread primarily written by Bruce Land ECE 4760 Final Project November - December, 2019 Portable ECG Monitor */ //////////////////////////////////// //////////////////////////////////// // Protothreads Header files //////////////////////////////////// #include "config_1_3_2.h" // threading library #include "pt_cornell_1_3_2.h" //////////////////////////////////// // graphics libraries //////////////////////////////////// #include "tft_master.h" #include "tft_gfx.h" //////////////////////////////////// // string buffer for printing char buffer[60]; // defining how often the adc/tft thread should run #define thread_yield 5 // 5 is 200 samples/sec and 3 is 333 samples/sec // === thread structures ============================================ // thread control structs // note that UART input and output are threads static struct pt pt_timer, pt_TFT; // === the fixed point macros ======================================== typedef signed int fix16 ; #define multfix16(a,b) ((fix16)(((( signed long long)(a))*(( signed long long)(b)))>>16)) //multiply two fixed 16:16 #define float2fix16(a) ((fix16)((a)*65536.0)) // 2^16 #define fix2float16(a) ((float)(a)/65536.0) #define fix2int16(a) ((int)((a)>>16)) #define int2fix16(a) ((fix16)((a)<<16)) #define divfix16(a,b) ((fix16)((((signed long long)(a)<<16)/(b)))) #define sqrtfix16(a) (float2fix16(sqrt(fix2float16(a)))) #define absfix16(a) abs(a) // GLOBAL VARIABLES static int adc_9; int top_input = -1; int top_output = -1; int top_deriv = -1; int top_detect = -1; int top_tval = -1; int stack_input[256]; int stack_output[256]; int stack_deriv[256]; int stack_detect[256]; int stack_time[2] = {0,0}; int stack_inc; /* STACK FUNCTIONS The following stacks are used for saving data for DSP. The top_x variables are for filling the stacks initially. The stacks are mostly all 256 samples just for fun, so we have lots of data to work with. This was mostly for experimenting during the development phase, but I'm going to keep it for now. */ void push_input (int x) { // for storing raw data from adc if (top_input < 255) { top_input = top_input + 1; stack_input[top_input] = x; } else if (top_input == 255) { for (stack_inc = 0; stack_inc < 255; stack_inc++) { stack_input[stack_inc] = stack_input[stack_inc + 1]; } stack_input[top_input] = x; } } void push_output (int y) { // for storing band-pass filter output if (top_output < 255) { top_output = top_output + 1; stack_output[top_output] = y; } else if (top_output == 255) { for (stack_inc = 0; stack_inc < 255; stack_inc++) { stack_output[stack_inc] = stack_output[stack_inc + 1]; } stack_output[top_output] = y; } } void push_deriv (int z) { // for storing derivative values for processing if (top_deriv < 255) { top_deriv = top_deriv + 1; stack_deriv[top_deriv] = z; } else if (top_deriv == 255) { for (stack_inc = 0; stack_inc < 255; stack_inc++) { stack_deriv[stack_inc] = stack_deriv[stack_inc + 1]; } stack_deriv[top_deriv] = z; } } void push_detect (int detect) { // for storing QRS detection samples if (top_detect < 255) { top_detect = top_detect + 1; stack_detect[top_detect] = detect; } else if (top_detect == 255) { for (stack_inc = 0; stack_inc < 255; stack_inc++) { stack_detect[stack_inc] = stack_detect[stack_inc + 1]; } stack_detect[top_detect] = detect; } } void push_time (int tval) { // for storing time values for HR calculations stack_time[0] = stack_time[1]; stack_time[1] = tval; } //////////////////////////////////// // TFT Plotting Thread //////////////////////////////////// static PT_THREAD (protothread_TFT(struct pt *pt)) { PT_BEGIN(pt); /* The TFT display has a upper left (x,y) of (0,0) and Rotation mode 1 is being used so that x is 320 and y is 240 */ // thread variables, to be defined once static int x_pos = 0; // scrolling variable for horizontal view (/360) static int y_pos; // data values static int low_samples = 8; // how many samples to average for low-pass av static int low_out; // low pass output static int high_out; // high pass output static int band_out; // low pass output - high pass output int n; // for loop variable static int deriv; // derivative value static int time = 0; // initialize time value for HR detection static int thresh; // threshold for QRS detection static float HR = 0; // heart rate variable static int beat_counter = 0; tft_fillRoundRect(0,0, 320, 240, 1, ILI9340_BLACK);// x,y,w,h,radius,color // for filling the screen initially in this thread tft_setCursor(10, 10); tft_setTextColor(ILI9340_YELLOW); tft_setTextSize(2); sprintf(buffer, "Waiting for Signal"); tft_writeString(buffer); while(1) { PT_YIELD_TIME_msec(thread_yield); // read the ADC adc_9 = ReadADC10(0); AcquireADC10(); // not needed if ADC_AUTO_SAMPLING_ON below push_input(adc_9); // add raw data value to stack /* Average 8 samples -- approximates a low pass filter and smooths the signal */ for (n = 0; n < low_samples + 1; n++) { low_out += stack_input[255 - n]; if (n == low_samples) { low_out = low_out >> 3; } } /* High pass filter approximated by averaging a bunch of samples, in this case 255, and subtracting that value off of the low-pass filter output. This is a way to remove DC offset, and it reduces sensitivity to slight movements. Overall at this point, we have a band-pass filter. The band-pass value is pushed into the output stack, which will be used in plotting on the TFT */ for (n = 0; n < 256; n++) { high_out += stack_input[255-n]; if (n == 255) { high_out = high_out >> 8; band_out = low_out - high_out; push_output(band_out); // store output to stack (bandpass) low_out = 0; // reset average to 0 for next time through } } /* To detect the QRS complex, and therefore a heartbeat, I take the difference between four points of the band-pass filter (approximates derivative) */ deriv = stack_output[255] - stack_output[251]; // take difference deriv = abs(deriv); // absolute value push_deriv(deriv); // store derivative value for processing below /* I averaged the derivative first, because there were still distinct bumps for each wave (esp. the QRS and T waves), but then decided to take another derivative as shown below, which ended up giving a better looking response for QRS/beat detection */ deriv = stack_deriv[255] - stack_deriv[254]; // another difference /* The derivative has a negative component, so here I set the deriv value to 0 to eliminate that, which gave a better response than squaring or something similar, because with squaring, it just produces two spikes, which you could then average into one peak, but the response provided by doing the algorithm here is more intense and easily detectable */ if (deriv < 0) { deriv = 0; } deriv = -deriv; //////////////////////////////// // detecting QRS and notifying //////////////////////////////// push_detect((deriv << 3) + 180); // push deriv value for beat detection // corrected for TFT position in the y direction thresh = 140; // lower part of the y direction, just past middle /* The following conditional statements check for -- (1) is the time that has gone by since the last detection greater than 100 ms, and (2) is the threshold between the newest detection sample and the sample 5 ago This double check confirms that it is indeed a NEW heartbeat and is required because the second condition can be true for a couple thread runs. Having that second condition check a few samples apart helps to ensure consistency in QRS detection */ if ((PT_GET_TIME() - stack_time[1]) > 100) { if ((stack_detect[255] > thresh) && (stack_detect[250] < thresh)) { time = PT_GET_TIME(); push_time(time); // save time in the stack for HR calculation /* Calculate HR using 1000/(time between beats) * 60 */ HR = (((float) 1000)/((float) abs(stack_time[1] - stack_time[0]))) * 60; /* The following conditional statements check if (1) the calculated HR is greater than 200 or less than 50, or if the output is erratic (no contact on the electrodes) and if so, the screen will display "waiting for signal" (2) otherwise the heart rate is shown */ if ((HR > 200) || (HR < 50) || ((((stack_output[255] - stack_output[245]) << 1) - 400) > 5)) { tft_fillRoundRect(0,0, 320, 40, 1, ILI9340_BLACK);// x,y,w,h,radius,color tft_setCursor(10, 10); tft_setTextColor(ILI9340_YELLOW); tft_setTextSize(2); sprintf(buffer, "Waiting for Signal"); tft_writeString(buffer); } else { tft_drawLine(x_pos - 5, 200, x_pos - 5, 240, ILI9340_GREEN); tft_fillRoundRect(0,0, 320, 40, 1, ILI9340_BLACK);// x,y,w,h,radius,color tft_setCursor(10, 10); tft_setTextColor(ILI9340_YELLOW); tft_setTextSize(2); sprintf(buffer, "Heart Rate = "); tft_writeString(buffer); tft_setCursor(160, 10); sprintf(buffer,"%3.1f", HR); tft_writeString(buffer); beat_counter++; tft_setCursor(290, 10); tft_setTextColor(ILI9340_RED); tft_setTextSize(2); sprintf(buffer,"%d", beat_counter); tft_writeString(buffer); } } } //////////////////////////////// // plotting data //////////////////////////////// y_pos = (stack_output[255] << 1) - 380; // for plotting //y_pos = (deriv << 3) + 180; //if you want to see the derivative tft_fillCircle(x_pos, y_pos, 1, ILI9340_CYAN); // signal data point /* The following rectangle serves as a scrolling eraser This allows the old signal to remain on the screen until being overwritten by the new signal */ tft_fillRoundRect(x_pos + 1,40,5,200,1,ILI9340_BLACK); /* The point of the following if else statement is to extend the time range that the signal is displayed. If x_pos incremented every time then the scroll would be very fast and you would only see about one and a half cycles of the ECG Therefore, to extend the time, we increment x_pos every other time the thread runs, which essentially compresses the data so you can see more This can be adjusted to compress or extend, aka increase or decrease the number of ECG cycles on the screen at once. To adjust, make the statement read if (increment == 0 || increment == 1) if you want more signals on the display at once, or remove the statement entirely and increment x_pos every runtime. As the if else statement currently stands, there is a nice amount of cycles on the screen at once (about 4-5 at resting rate aka 60 to 70 bpm). */ /* if (increment == 0) { x_pos = x_pos; increment++; } else { x_pos++; increment = 0; } */ x_pos++; // shows faster scroll and more resolution of each signal /* This statement resets the x_pos when you get to the right side of the screen */ if (x_pos > 319) { x_pos = 0; } } PT_END(pt); } // === Main ====================================================== void main(void) { //SYSTEMConfigPerformance(PBCLK); ANSELA = 0; ANSELB = 0; // === config threads ========== // turns OFF UART support and debugger pin, unless defines are set PT_setup(); // === setup system wide interrupts ======== INTEnableSystemMultiVectoredInt(); // the ADC /////////////////////////////////////// // configure and enable the ADC CloseADC10(); // ensure the ADC is off before setting the configuration // define setup parameters for OpenADC10 // Turn module on | ouput in integer | trigger mode auto | enable autosample // ADC_CLK_AUTO -- Internal counter ends sampling and starts conversion (Auto convert) // ADC_AUTO_SAMPLING_ON -- Sampling begins immediately after last conversion completes; SAMP bit is automatically set // ADC_AUTO_SAMPLING_OFF -- Sampling begins with AcquireADC10(); #define PARAM1 ADC_FORMAT_INTG16 | ADC_CLK_AUTO | ADC_AUTO_SAMPLING_OFF // // define setup parameters for OpenADC10 // ADC ref external | disable offset test | disable scan mode | do 1 sample | use single buf | alternate mode off #define PARAM2 ADC_VREF_AVDD_AVSS | ADC_OFFSET_CAL_DISABLE | ADC_SCAN_OFF | ADC_SAMPLES_PER_INT_1 | ADC_ALT_BUF_OFF | ADC_ALT_INPUT_OFF // // Define setup parameters for OpenADC10 // use peripherial bus clock | set sample time | set ADC clock divider // ADC_CONV_CLK_Tcy2 means divide CLK_PB by 2 (max speed) // ADC_SAMPLE_TIME_5 seems to work with a source resistance < 1kohm #define PARAM3 ADC_CONV_CLK_PB | ADC_SAMPLE_TIME_5 | ADC_CONV_CLK_Tcy2 //ADC_SAMPLE_TIME_15| ADC_CONV_CLK_Tcy2 // define setup parameters for OpenADC10 // set AN11 and as analog inputs #define PARAM4 ENABLE_AN11_ANA // pin 24 // define setup parameters for OpenADC10 // do not assign channels to scan #define PARAM5 SKIP_SCAN_ALL // use ground as neg ref for A | use AN11 for input A // configure to sample AN11 SetChanADC10( ADC_CH0_NEG_SAMPLEA_NVREF | ADC_CH0_POS_SAMPLEA_AN11 ); // configure to sample AN11 OpenADC10( PARAM1, PARAM2, PARAM3, PARAM4, PARAM5 ); // configure ADC using the parameters defined above EnableADC10(); // Enable the ADC /////////////////////////////////////////////////////// // init the threads PT_INIT(&pt_TFT); // init the display tft_init_hw(); tft_begin(); tft_fillScreen(ILI9340_BLACK); //240x320 vertical display tft_setRotation(1); // Use tft_setRotation(1) for 320x240 // seed random color srand(1); // round-robin scheduler for threads while (1){ PT_SCHEDULE(protothread_TFT(&pt_TFT)); } } // main // === FIN ======================================================