Quantcast
Channel: MSP430 Technical Forums
Viewing all articles
Browse latest Browse all 2077

SubiCount: an improved tally counter

$
0
0

My day job is a rheumatologist and immunology researcher. An important but stupid-boring part of research involves an accurate counting of cells. Whether you work in  immunology, cancer biology, sperm counting, etc, an accurate cell count is essential for meaningful and reproducible results. There are electronic Coulter counters but they are expensive and unless you are constantly counting cells, it doesn't make sense because of the constant adjusting and cleaning involved.

 

When I was first shown how to count cells, I thought they were taking the new guy on a snipe hunt! You take a suspension of cells and put 10 microliters (ul) to a specially calibrated slide called a hemocytometer. It has a calibrated grid of lines and a precision ground spacing between the slide and coverslip so that the volume of fluid being examined is precisely known. You typically count the number of cells in a 1 x 1 x 0.1 mm (0.1 ul) volume. So for example if you count 100 cells, you have 100/0.1ul = 1 million cells/ml.

 

See http://en.wikipedia.org/wiki/Hemocytometer for pictures and an explanation. If you want accurate and reproducible results, you need to do a good job. Once you learn few rules, it is easy but at the same time mind numbing and tedious.

 

A friend of mine did research on subtizing (https://en.wikipedia.org/wiki/Subitizing). Whats that you say? If I flash up fingers for a fraction of a second, you don't count the individual fingers-- you instantly recognize the number. Most people can subitize up to ~5 or 6 items and as you can imagine, it is much faster than counting. I wanted to see if I could subitize to speed up counting. Enter the SubiCount.

 

The hardware is rather simple so I routed the board manually, i.e. without a schematic. Two transistors, two resistors and a bypass cap. The first version of the firmware was not particularly power efficient-- it was constantly in LPM3 with a timer driven interrupt routine. About 100 times per second, it would run a button debounce routine, take care of sending pulses to the tally counter, and fall back into LPM3. Pulses to the tally counter need to be sent at a controlled rate otherwise pulses get lost. The MSP430 keeps track of how many need to be streamed to the tally counter.

 

Problem was, it would do this 24/7 regardless of whether it was being used or not. Despite this, the two button cell batteries lasted three and a half months. For my project entry, I wanted to really leverage the low power aspect of the MSP430. I haven't had a chance to see what the new battery life is but I reckon it will likely be several years because it spends the bulk of its time in LPM4.

 

Here is a video of SubiCount in action:

https://www.youtube.com/watch?v=_fc95bzSDj4

 

My implementation was to stay in LPM4. From LPM4, pressing any of the buttons will trigger a PORT1 interrupt which puts the 430 into LPM3 and turns off the PORT1 interrupt. In LPM3, a timer driven interrupt routine is called 100 times per second to debounce buttons and send pulses to the tally counter. When all the pulses are sent and no button presses are active or pending, the PORT1 interrupt is turned back on and goes into LPM4.

 

Here is the code for main. It does nothing other than turning on global interrupts and going into LPM3.

int main(void)
{
    Grace_init();                   // Activate Grace-generated configuration
    
    // >>>>> Fill-in user code here <<<<<
    _BIS_SR(LPM3_bits + GIE);
    // never should get here
}

The meat is in two interrupt routines: one for PORT1 and another for TimerA0. I used GRACE so the code is in the automatically generated InterruptVectors_init.c file. Comments have been edited out for brevity:

#include <msp430.h>
/* SubiCount by Peter Kim 2013. Creative Commons License: Attribution-NonCommercial-ShareAlike 4.0 International */


/* USER CODE START (section: InterruptVectors_init_c_prologue) */
/* User defined includes, defines, global variables and functions */

#define MAXDEBOUNCE 2	//threshold for debounce
#define MAXRESET 300	// threshold for three button reset

int processKey(unsigned int pinValue, unsigned int *pkeyInteg, unsigned int *plastValue);

int i;

unsigned int oneKeyInteg = 0;
unsigned int twoKeyInteg = 0;
unsigned int threeKeyInteg = 0;
unsigned int resetInteg = 0;

unsigned int oneKeyLastVal = 0;
unsigned int twoKeyLastVal = 0;
unsigned int threeKeyLastVal = 0;

volatile unsigned int pulses;

int processKey(unsigned int pinValue, unsigned int *pkeyInteg, unsigned int *plastValue) {
/* Debounce routine adapted from http://www.kennethkuhn.com/electronics/debounce.c
*/
  int posEdge = 0;

  if (pinValue)  //pinValue inverted
  {
    if (*pkeyInteg > 0)
      (*pkeyInteg)--;
  }
  else if (*pkeyInteg < MAXDEBOUNCE)
    (*pkeyInteg)++;

  if (*pkeyInteg == 0)
    *plastValue = 0;
  else if (*pkeyInteg >= MAXDEBOUNCE && *plastValue == 0)
  {
		posEdge = 1;
		*plastValue = 1;
		*pkeyInteg = MAXDEBOUNCE;  /* defensive code if integrator got corrupted */
  }

  return(posEdge);
}

void InterruptVectors_graceInit(void)
{
}

#pragma vector=PORT1_VECTOR
__interrupt void PORT1_ISR_HOOK(void)
{
    /* USER CODE START (section: PORT1_ISR_HOOK) */
	P1IFG &= ~(BIT3+BIT2+BIT1);	//clear interrupt flag
	P1IE &= ~(BIT3+BIT2+BIT1);		//clear P1.3 interrupt enable


	__bic_SR_register_on_exit(OSCOFF);	//put into LPM3
    /* USER CODE END (section: PORT1_ISR_HOOK) */
}

/*
 *  ======== Timer_A2 Interrupt Service Routine ======== 
 */
#pragma vector=TIMERA0_VECTOR
__interrupt void TIMERA0_ISR_HOOK(void)
{
    /* USER CODE START (section: TIMERA0_ISR_HOOK) */
	static int count =0; // to keep track of times here
	int P1;

	P1 = P1IN;

	if ( processKey(P1 & BIT1, &oneKeyInteg, &oneKeyLastVal) )
		pulses++;
	if ( processKey(P1 & BIT2, &twoKeyInteg, &twoKeyLastVal) ) {
		pulses += 2;
	}
	if ( processKey(P1 & BIT3, &threeKeyInteg, &threeKeyLastVal) ) {
		pulses += 3;
	}
	if ( (P1 & (BIT1|BIT2|BIT3)) == 0) {
		resetInteg++;
		if (resetInteg > MAXRESET) {
			resetInteg = 0;
			P1OUT |= BIT6; //turn on bit6, reset
			_delay_cycles(40000);
			P1OUT = BIT1|BIT2|BIT3; //need pull ups on
			pulses = 0;		//reset pulses count after reset
			return;
		}
	} else if (resetInteg)
		resetInteg--;

	if (count++ >7)
	{
		count = 0;
		if (P1OUT & BIT0)   //count pin high?
		{
			P1OUT &= ~BIT0; //then turn off
			pulses--;		//and decrement pulses
		} else if (pulses) {
			P1OUT |= BIT0;	//turn on count pin
		}
	}

	if (pulses == 0)	// done with pulses?
	{
		if ( (P1IN & BIT1+BIT2+BIT3) == BIT1+BIT2+BIT3 )	//are all buttons not pressed?
		{
			if (oneKeyLastVal==0 && twoKeyLastVal==0 && threeKeyLastVal==0)
			{
				P1IFG &= ~(BIT3+BIT2+BIT1);	//clear P1.3 int flag
				P1IE |= BIT3+BIT2+BIT1;		//enable P1.3 interrupt
				__bis_SR_register_on_exit(LPM4_bits + GIE);	//go into LPM4
			}
		}
	}
    /* USER CODE END (section: TIMERA0_ISR_HOOK) */
}
 

Here is the hand routed board:

cell count.png

 

And a hand drawn schematic:

schematic.jpg


Viewing all articles
Browse latest Browse all 2077

Trending Articles