Close

NeuroScope, a real-time membrane potential waveform viewer

A project log for NeuroBytes

Build your own nervous system!

zakqwyzakqwy 07/15/2016 at 15:040 Comments

This past weekend I built the first prototype of a new NeuroBytes accessory:

Shaky video:

The NeuroScope project has two main components:

  1. Modifications to the NeuroBytes v0.8 IAF code to (a) lock the main program loop to a stable timebase; (b) convert the current membrane potential to an unsigned 8-bit integer; and (c) send said 8-bit value via UART through one of the axon connectors.
  2. #Teensy 3.2 pulling in UART data, graphing it on the QVGA TFT LCD, and handling the three input buttons for chart speed and fire counter reset. This is the unit I have left over from @Paul Stoffregen's fantastic #Microcontroller Audio Workshop & HaD Supercon 2015 workshop. He's done great work getting the display to play nice with the Teensy and the Adafruit GFX library, so consider checking it out if you want to quickly add a high-res display to a project.

This is still very much a prototype and the NeuroBytes code isn't integrated into the standard runtime firmware; I still need to figure out how the user will ultimately get data out of the boards, as tying up one of the two axon connectors may not be ideal. A few goals for the next version:

  1. Improve the serial output function in the NeuroBytes firmware. Adding the timing lock meant dramatically reducing the LED update rate from ~160 Hz to ~70 Hz, as I'm still getting a few pesky delays in the firing routine. I'm working on these issues separately; the plan is to run the LED update as a timer interrupt, but it's still a work in progress. I might also try to expand the bit width of the membrane potential output so I don't need to downconvert the value in firmware.
  2. Improve the data acquisition code. The serial conversion is a bit glitchy (as shown in the image above), which I believe is traceable to a problem on the Teensy side. Not a huge deal as the program does work, but I think it should be smoother. Also the rate control for the chart doesn't work (although the buttons do change the displayed value).
  3. Add several channels. The Teensy has three serial ports but for some reason I was only able to get one to work at my funky baud rate. I may have nuked the hardware by accident, but I'm also thinking about other data processing options; since I'm using the internal RC oscillators on the NeuroBytes boards they tend to vary by a decent amount, meaning the NeuroScope needs to handle multiple streams of UART data at slightly different baud rates. I haven't stuck a toe into the FPGA world but I think this could be a great application for an iCE40 or a similar low-price device. If I can offload input signal consolidation to that platform and then send the raw data into the Teensy via higher speed SPI (or something), I should be able to support tons of channels without too much trouble (ha!).
  4. Datalogging. I'd like to log the data directly to an SD card in *.csv format for offline analysis. I think this mght be useful for running longer term experiments and it shouldn't be a huge processor burden.
  5. Rate measurement. Right now I count action potentials by looking for the MP to exceed a threshold. It should be fairly simple to convert this to a moving-average rate display, which will also greatly help with more advanced experimentation.

Here are the code modifications for the NeuroBytes boards (only the changes are shown for simplicity), along with the complete Teensy program. As mentioned above, this is a prototype so I don't suggest using this code yourself, but if you want to it's all covered under GPL v3.0 (as with the rest of the project). The code snippits are the last item in this post, so feel free to stop reading here if you like.

NeuroBytes code

convert membrane potential value:

			else if (t_var_timer == 20) {
			//	added scope scaling deal-o
			//	o_var_8bit = (uint8_t)((n_var_v + o_con_offset) << 8);
			//	o_var_8bit = (uint8_t)((n_var_v) << 8);	
				if (t_var_fire < t_var_fire_reset) {
					o_var_8bit = 200;
				}
				else {
					o_var_8bit = (uint8_t)((n_var_v + 250 - 32767) >> 2);
				}
... UART out on axon:

else if (t_var_timer == 21) {
				if (o_var_serial_count == 0) {
					PORTB |= (1<<PB2);
				}
				if (o_var_serial_count == 1) {
					PORTB &= ~(1<<PB2);
				}
				else if ((o_var_serial_count > 1) && (o_var_serial_count <= 9)) {
					if ((o_var_8bit & (1<<(o_var_serial_count - 2))) > 0) {
						PORTB |= (1<<PB2);
					}
					else {
						PORTB &= ~(1<<PB2);
					}
				}
				else if (o_var_serial_count > 9) {
					PORTB &= ~(1<<PB2);
				}

				if (o_var_serial_count == 15) {
					o_var_serial_count = 0;
				}
				else {
					o_var_serial_count++;
				}

... ISR and timer setup:

ISR(TIMER0_COMPA_vect) {
	t_var_tick = 1;
}

/* set up Timer/Counter0 for main loop timing */
	TCCR0A |= (1<<CTC0); //clear timer compare mode
	TCCR0A |= (1<<CS01); //prescale to clk/8 (1 MHz)
	OCR0A = 60;
	TIMSK0 |= (1<<OCIE0A); //enable Compare Match A interrupt

int main(void) {
	systemInit();
	uint8_t i;
/*	
	for (i=0;i<60;i++) {
		n_var_v_delay[i] = 32767;
	}
*/
	for(;;) {
		while (t_var_tick == 0) {}
		cli();
		t_var_tick = 0;
[... ]

Teensy code:

#include <ILI9341_t3.h>
#include <SPI.h>
#include <font_Arial.h>
#include <font_ArialBold.h>

#define TFT_DC    21
#define TFT_CS    20
#define TFT_RST   255
#define TFT_MOSI  11
#define TFT_SCLK  14
#define TFT_MISO  12
#define BUTTON0   19
#define BUTTON1   16
#define BUTTON2   18

ILI9341_t3 tft = ILI9341_t3(TFT_CS, TFT_DC, TFT_RST, TFT_MOSI, TFT_SCLK, TFT_MISO);

unsigned long baud_rate;
unsigned long baud_rate_prev = 0;
int incomingByte;
int incomingBytePrev;
int i = 0;
int i_prev = 0;
int ap_count = 0;
int ap_count_prev = -1;
int ap_reset = 0;
int spm = 16;
int spm_prev = 0;
int button1_prev = 0;
int button2_prev = 0;

void drawBorders_2ch() {
  tft.drawFastHLine(0,0,239,ILI9341_WHITE);
  tft.drawFastHLine(0,158,239,ILI9341_WHITE);
  tft.drawFastHLine(0,318,239,ILI9341_WHITE);
  tft.drawFastHLine(51,79,189,ILI9341_DARKGREY);
  tft.drawFastHLine(51,237,189,ILI9341_DARKGREY);
  tft.drawFastVLine(50,0,319,ILI9341_WHITE);
}

void drawLabels_2ch() {
  tft.setTextColor(ILI9341_GREEN);
  tft.setFont(Arial_14);
  tft.setCursor(5,73);
  tft.print("CH1");
  tft.setTextColor(ILI9341_RED);
  tft.setCursor(5,231);
  tft.print("CH2");
}

void refreshGraphs_2ch() {
  tft.fillRect(51,1,190,157,ILI9341_BLACK);
  tft.fillRect(51,159,190,158,ILI9341_BLACK);
  tft.drawFastHLine(51,79,189,ILI9341_DARKGREY);
  tft.drawFastHLine(51,237,189,ILI9341_DARKGREY);
}

void drawBorders_1ch() {
  tft.drawFastVLine(226,0,319,ILI9341_WHITE);
  tft.drawFastVLine(188,176,20,ILI9341_WHITE);
  tft.drawFastVLine(188,280,20,ILI9341_WHITE);
  tft.drawFastHLine(188,176,45,ILI9341_WHITE);
  tft.drawFastHLine(188,300,45,ILI9341_WHITE);
  tft.drawFastHLine(157,264,63,ILI9341_WHITE);
  tft.drawFastHLine(0,120,226,ILI9341_DARKGREY);
}

void drawLabels_1ch() {
  tft.setTextColor(ILI9341_WHITE);
  tft.setFont(Arial_9);
  tft.setRotation(135);
  tft.setCursor(253,229);
  tft.print("RESET");
  tft.setRotation(0);
  tft.setCursor(230,171);
  tft.print("+");
  tft.setCursor(231,288);
  tft.print("_");
  tft.setFont(Arial_9);
  tft.setCursor(166,199);
  tft.print("CHART");
  tft.setCursor(166,211);
  tft.print("SPEED");
  tft.setCursor(157,252);
  tft.print("SCREENS");
  tft.setCursor(165,267);
  tft.print("MINUTE");
  tft.setCursor(5,185);
  tft.print("MEMBRANE");
  tft.setCursor(10,197);
  tft.print("POTENTIAL");
  tft.setCursor(52,228);
  tft.print("FIRE");
  tft.setCursor(36,240);
  tft.print("COUNT");
  tft.setCursor(44,271);
  tft.print("BAUD");
  tft.setCursor(46,283);
  tft.print("RATE");
  
}

void refreshData_1ch() {
  tft.setFont(Arial_16_Bold);
  tft.setTextColor(ILI9341_WHITE);
  
  if (spm != spm_prev) {
    tft.fillRect(175,228,40,16,ILI9341_BLACK);
    tft.setCursor(175,228);
    tft.print(spm);
    spm_prev = spm;
  }

  if (incomingBytePrev != incomingByte) {
    tft.fillRect(90,189,60,16,ILI9341_BLACK);
    tft.setCursor(90,189);
    tft.print(incomingByte);
  }

  if (ap_count != ap_count_prev) {
    tft.fillRect(90,231,40,16,ILI9341_BLACK);
    tft.setCursor(90,231);
    tft.print(ap_count); 
    ap_count_prev = ap_count;
  }

  if (baud_rate != baud_rate_prev) {
    tft.fillRect(90,273,60,16,ILI9341_BLACK);
    tft.setCursor(90,273);
    tft.print(baud_rate);
    baud_rate_prev = baud_rate;
  }
  
  tft.setFont(Arial_9);
}

void refreshGraphs_1ch() {
  tft.fillRect(0,0,226,157,ILI9341_BLACK);
  tft.drawFastHLine(0,120,226,ILI9341_DARKGREY);
}

void updateButtonGraphic(int buttonID, int state) {
/*
 * buttonID: 0 = top, 1 = middle, 2 = bottom
 * state: 0 = released, 1 = pressed
 */
 
 switch (buttonID) {
  case 0:
    tft.setRotation(135);
    if (state == 0) {
      tft.setTextColor(ILI9341_WHITE);
      tft.fillRect(250,227,50,13,ILI9341_BLACK);
    }
    else {
      tft.setTextColor(ILI9341_BLACK);
      tft.fillRect(250,227,50,13,ILI9341_WHITE);
    }
    tft.setFont(Arial_9);
    tft.setCursor(253,229);
    tft.print("RESET");
    tft.setRotation(0);
    break;
  case 1:
    if (state == 0) {
      tft.setTextColor(ILI9341_WHITE);
      tft.fillRect(227,151,13,50,ILI9341_BLACK);
    }
    else {
      tft.setTextColor(ILI9341_BLACK);
      tft.fillRect(227,151,13,50,ILI9341_WHITE);
    }
    tft.setFont(Arial_9);
    tft.setCursor(230,171);
    tft.print("+");
    break;
  case 2:
    if (state == 0) {
      tft.setTextColor(ILI9341_WHITE);
      tft.fillRect(227,275,13,50,ILI9341_BLACK);
    }
    else {
      tft.setTextColor(ILI9341_BLACK);
      tft.fillRect(227,275,13,50,ILI9341_WHITE);
    }
    tft.setFont(Arial_9);
    tft.setCursor(231,288);
    tft.print("_");
    break;
 }
}

void addPoint(int ch, int t, int val_raw) {
  int x = t + 51;
  int y;
  if (ch == 1) {
    y = 79 + 62 - val_raw;
    tft.drawPixel(x,y,ILI9341_GREEN);
  }
  else if (ch == 2) {
    y = 237 + 62 - val_raw;
    tft.drawPixel(x,y,ILI9341_RED);
  }
}

void addConnectedPoint(int ch, int t, int t_prev, int val_raw, int val_raw_prev) {
  int x = t;
  int x_prev = t_prev;
  int y;
  int y_prev;
  if (x_prev < x) {
    if (ch == 1) {
      y = 120 + 31 - (val_raw >> 1);
      y_prev = 120 + 31 - (val_raw_prev >> 1);
      tft.drawLine(x_prev,y_prev,x,y,ILI9341_GREEN);      
    }
  }
}

void setup() {
  delay(500);
  tft.begin();
  tft.fillScreen(ILI9341_BLACK);
  drawBorders_1ch();
  drawLabels_1ch();
  pinMode(BUTTON0, INPUT);    
  pinMode(BUTTON1, INPUT);
  pinMode(BUTTON2, INPUT);
  pinMode(1, INPUT);
  baud_rate = 765;
  Serial3.begin(baud_rate);
}


void loop() {
  //baud_rate = map(analogRead(1),150,870,650,800);
  //Serial3.begin(baud_rate);
  if (Serial3.available()) {
    incomingBytePrev = incomingByte;
    incomingByte = Serial3.read();
  }
  if ((incomingByte > 110) && (ap_reset ==  0)) {
    ap_count++;
    ap_reset = 1;
    tft.drawFastVLine(i,10,20,ILI9341_RED);
  }
  if ((incomingByte < 50) && (ap_reset == 1)) {
    ap_reset = 0;
  }

  if ((digitalRead(BUTTON1) && (button1_prev == 0)) && (spm < 64)) {
    spm<<=1;
  }
  if ((digitalRead(BUTTON2) && (button2_prev == 0)) && (spm > 1)) {
    spm>>=1;
  }
  button1_prev = digitalRead(BUTTON1);
  button2_prev = digitalRead(BUTTON2);  
  

  if (digitalRead(BUTTON0)) {
    ap_count_prev = ap_count;
    ap_count = 0;
  }
  
  updateButtonGraphic(0,digitalRead(BUTTON0));
  updateButtonGraphic(1,digitalRead(BUTTON1));
  updateButtonGraphic(2,digitalRead(BUTTON2));
  refreshData_1ch();
  addConnectedPoint(1,i,i_prev,incomingByte,incomingBytePrev);  
  i_prev = i;
  i++;
  if (i == 226) {
    i = 0;
    refreshGraphs_1ch();
  }
  if (i == 0) {
    i_prev = 226;
  }
  delay(10);
}

Discussions