Introduction
The nervous systems of animals (and humans) are complex electrochemical machines.
The electrophysiological study of a nervous system treats the system as an electrical
device built of very complex, non-linear elements called neurons. To understand
the function of a neuron, or network of neurons, one must probe the system with
pulses and record what happens. For instance, a researcher might apply pulses
to a nerve and measure muscle contraction. The device to be described here allows
a researcher to inject electrical pulses into neural tissue. The biological
jargon-term for this device is a "stimulator". The output of the stimulator
must:
Ten years ago, a stimulator was an analog device built with 555 timers, switches and potentiometer knobs. The analog version was limited to a few waveforms, typically one or two pulses of the same duration separated by a variable delay and recurring at a fixed rate. Isolation was achieved by using optocouplers and batteries. Now it makes more sense to use a microcontroller to generate the pulses and a PC to generate the user interface. The microcontroller can guarantee a real-time response, while the PC has tools to produce a convenient user interface. Since we intend to use these stimulators in a teaching context, we wanted to eliminate all batteries from the isolator section. Teaching makes large demands on batteries because of the daily use and the unfamiliarity of the equipment to the students. The combination of ubiquitous PC for control, microcontroller for pulse generation and a novel isolation circuit makes the circuit cost-competitive for student laboratory use.
The following block diagram summarizes the stimulator circuit. A single transistor
is used to interface RS232 levels (receive only) to the UART input of the microcontroller.
A Matlab program running on the PC sends simple commands to the microcontroller
to set up pulse timing and amplitude. The microcontroller produces a pulse-width
modulated (PWM) output proportional to the desired amplitude. The PWM output
is lowpass filtered and converted to a current to drive an optocoupler. The
optocoupler has a photoFET output which, when used in a voltage divider, (see
full schematic) produces a voltage output which is almost linear (+/-5% full
scale) with the PWM input. Power for the isolator is controlled by a timing
pulse from the microcontroller that rapidly gates a transformer-isolated, 14
pin DIP, DC-to-DC converter. Pulse rise and fall times time are about 50 microseconds,
much faster than the bandwidth of the neurons being stimulated. The circuit
can provide about a watt to the output, sufficient for most experimental setups.
The microcontroller emits a synch pulse at the start of each pulse train repetition.
Pulse train repetition is either periodic (and set by the user interface) or
manually triggered.
Project Details
Specifications
An electrophysiological stimulator needs to produce a (possibly repeating) pulse train. Pulses are acceptable for most research since the active elements of the nervous system, called neurons, are charge integrators. Said differently, the details of the waveform are unimportant as long as the product of average current and time reach some threshold value. The stimulator to be described here can:
Figure 1 summarizes the waveforms generated. The synch pulse is emitted from
a port pin with duration of about 16 machine cycles (2 microseconds). The synch
pulse is typically used to trigger an oscilloscope. The 'repeat time' shown
represents the time between pulse trains, if the GUI sets the mode to repeat.
The main output is the pulse train which consists of a series of variable amplitude
pulses of a specified duration and delay.
AVR Software
An Atmel AVR AT90s8515 was used to control the timing of pulses and pulse amplitude as well as the serial command interface from the PC. All programming was done in CodeVision C. The output circuitry controlled by the AT90s8515 implements the pulse generation and electrical isolation.
The C program running on the AVR (see Appendix A) sets up i/o functions and state variables, then defines a main loop and one interrupt service routine (ISR). Details follow:
g start generating pulses in repeat mode, enable pushbutton
in manual modes stop generating pulses in repeat mode, disable pushbutton
in manual modemm set manual modemr set repeat modep number amp duration spacing repeat set parameters.p 3 50 10 20 1000GUI software on the PC
A Matlab (mathworks.com) program running on the PC was written to provide the GUI and provide commands via the serial interface (see Appendix B). For this simple control application I defined five edit fields and a few pushbuttons (see screen dump in Appendix B) to control the stimulator. The program structure is simple: Define the controls, then enter an event loop and wait for controls to be touched. Each of the controls has a "callback" function that I used here for range checking and for setting an execution flag. The main loop mostly formats strings to send to the AVR, based on the numerical contents of the various edit fields. The GUI program handles unit conversion, and locks out changes in the parameters during pulse train generation.
Circuitry
As shown in Figure 3, the AT90s8515 was connected in a standard fashion with an eight MHz crystal and with the reset line pulled up with a 100kW resistor. A diode-clamped NPN transistor was used to invert and level-shift the RS232 signal from the PC. The manual trigger pushbutton (normally open) was connected from portD.2 to ground, with the internal pullup resistor turned on. An 8 MHz clock yields an overflow time of 32 microseconds for timer 0, which becomes the resolution of the pulse width. The GUI software rounds all pulses to 0.1 millisecond, or 4% greater than 3 overflow times. Thus, even short pulses have a relative accuracy of 4%.
Two outputs from the AVR are used to control aspects of the stimulator pulses. The timer 1, channel A, PWM output (port D.5) controls the pulse amplitude. The port pin D.3 is toggled by the timer 0 ISR to control pulse timing. PortD.4 is the synch pulse output for external connection to an oscilloscope or other recording device.
The PWM output is low-pass filtered with a simple RC circuit with a time constant of 0.01 second. Typically, amplitude would be changed only between pulse trains, so 0.01 seconds is fast enough. The filtered voltage varies from 0-2.5 volts, depending on the PWM setting (0-128). The full PWM range of 0-255 was not used because the opamp feedback circuit cannot follow inputs above about 3 volts (due to voltage drops across the transistor and optocoupler LED), while keeping the current constant through the optocoupler. For a 0-2.5 volt input, the opamp/transistor feedback loop produces 0-30 milliamps through the optocoupler LED.
Timing control for the pulses operates by gating the Burr-Brown/TI DCP010515D DC-to-DC converter using the "SYNCH-in" pin. This scheme has the advantage of zero offset error between pulses, which is important for tissue and electrode integrity. The DC-to-DC converter is turned on by floating its SYNCH-in pin and turned off by grounding the pin. The 4066 quad CMOS transmission gate is used as an inverter and as a switched ground connection to the converter SYNCH-in pin.
Together the DC-to-DC converter and the H11F1 optocoupler isolate the output
pulse from all external circuitry. Their effect is to act as a rapidly switched
voltage source and a more slowly set voltage divider to produce and attenuate
the final output pulse. The 20KW resistor across the output loads the DC-to-DC
converter to help with voltage regulation. The H11F1 and the 10kohm resistor
act as a voltage divider.
Performance
The GUI and 8515 can be started in any order because the 8515 waits for a command from the GUI, and the GUI sends only self-contained commands. Of course, if you cycle the 8515 power while it is actually receiving a command, it may end up in an indeterminate state.
Actual measured pulse rise times were around 50 microseconds, but varied somewhat
with amplitude from 40 to 70 microseconds. Pulse duration and delay times were
within the expected 4% maximum error for a short pulse. Figure 4 shows the linearity
of the amplitude control. The largest variation from the best-fit straight line
is about 5% of full output voltage.
Photographs
A photograph of the whole stimulator shows that the device was built on two
circuit boards. This allows a biological researcher to place the noisy microcontroller
away from the experiment, while allowing the isolated pulse output to be as
near the experiment as possible.
A closeup of the isolator output with clip leads attached.
A closeup of the AT90s8515.
A photograph of the oscilloscope screen shows the synch pulse on the top channel (in the center of the screen) and a train of two pulses on the bottom channel. Pulse length was set to 2 milliseconds and delay was set to 3 milliseconds. Gain settings are shown at the bottom of the frame.
Conclusion
This microcontroller-driven stimulator provides a flexible and inexpensive
solution for our biology teaching needs. We intend to deploy these stimulators
in student labs by next spring.
Appendix A. AT90s8515 C program
//Stimulator version 2.0
/*
Pulse train:
1-255 pulses
0-4 sec pulse duration
0-4 sec pulse spacing
0-4 sec train repeat time
0-30 volt amplitude
Pulse output is port D.3
Synch output is port D.4
Trigger is either manual or periodic
Manual trigger is port D.2
timer 0 ISR generates pulse timing
timer 1 generates PWM for amplitude control on pin OC1A (D.5)
continuously so lowpass filter can average it
main loop handles parameter setting and rs232 comm
*/
#include < 90s8515.h>
#include < Stdio.h>
#include < delay.h>
#include < stdlib.h>
#include < math.h>
#define begin {
#define end }
//input parameters
unsigned char num; //number of pulses
unsigned int dur; //pulse duration
unsigned long durL; //holds duration string for float convert
unsigned int delay; //pulse spacing
unsigned long delayL; //holds delay string
unsigned int rep; //pulse train repeat time
unsigned long repL; //holds repeat time string
unsigned int amp; //pulse amplitude 0-255
//state variables
unsigned int elapsedT; //running stimulus time
unsigned int nextT; //next event time
unsigned char mode; //manual or repeat 'm' or 'r'
unsigned char pulseon; //state machine variable 1 or 0
unsigned char currentpulse;//current pulse num
unsigned char starting; //indicates first time thru ISR
unsigned char cmd; //serial command character
float Conversion; //converts mSec to units of 64 microSec
//#pragma savereg-
//********************************************
//timer 0 overflow ISR
interrupt [TIM0_OVF] void t0_overflow(void)
begin
//check for starting and emit synch pulse, reset time
if (starting)
begin
PORTD.4 = 1; //start synch pulse output
starting = 0; //on next ISR we won't be starting
elapsedT = 0; //relative to pulse train start
pulseon = 0; //starts in the off state
nextT = delay; //next deadline time
currentpulse = 0; //the pulse number
PORTD.4 = 0; //end synch pulse
end
else //not starting
begin
elapsedT++; //all times are in 't0 ovfl tick units'
//at end of pulse train:
//check for manual mode and kill timer
//otherwise set starting back to 1 for next train
if (elapsedT == rep)
begin
PORTD.3 == 0; //kill the output pulse just in case
if (mode == 'r')
starting=1; //get ready to restart
if (mode == 'm')
TCCR0=0; //kill this ISR
end
else if (elapsedT == nextT)
begin
//have we putput all of the pulses?
if (currentpulse < num)
begin
if (pulseon)
begin //turn off the pulse
pulseon = 0;
PORTD.3 = 0;
nextT = elapsedT + delay;
end
else
begin //turn on the pulse
pulseon = 1;
PORTD.3 = 1;
currentpulse++; //inc the pulse cout
nextT = elapsedT + dur;
end //pulseon else
end
else //(currentpulse==num) so the train is done
begin
PORTD.3 = 0; //kill the last pulse
nextT = 0; //impossible time marker so next match is rep
end
end //if elapsedT
end //else not starting
end //ISR
//
//*********************************************
//#pragma savereg+
void main(void)
begin
//serial comm setup (no interrupts)
UCR = 0x18 ;
UBRR = 51; //25 ; 4 MHz value
//putsf("\rStimulator 2.0 copyright Cornell University\r");
//timer 0 setup
TIMSK = 0x02 ; //Timer 0 ovfl enable
TCCR0 = 0; //Timer 0 is off
//timer 1 setup (rest of setup is in ISR)
TCCR1B = 0; //Timer 1 is off
//port D setup
DDRD.2 = 0 ; //port D.2 is trigger input
PORTD.2 = 1; //turn on D.2 internal pullup
DDRD.5 = 1 ; //port D.5 is PWM output
DDRD.3 = 1; //port D.3 is main pulse
DDRD.4 = 1; //port D.4 is synch pulse
//Convert 10*mSec to units of 64 microSec
// 32
Conversion = 1000.0/32.0/10 ; // 1000.0/64.0/10 ;
//turn on all interrupts
#asm
sei
#endasm
/*main event loop
two events:
serial command input
--s stop
--g go
--p set parameters number,amplitude,dur,delay,rep
--m set mode manual/repeat
starting pushbutton in manual mode
*/
while(1)
begin
//if manual, check button D.2 for a trigger (active-low)
if (mode=='m' && PIND.2==0)
begin
TCNT0=0;
TCCR0=1;
starting=1;
//debounce the switch and wait for release
delay_ms(50);
while(PIND.2==0){};
delay_ms(50);
end //pushbutton event
//check USR for a valid character and get it
if (USR.7==1)
begin
cmd=getchar();
switch (cmd)
begin
case 's':
//to stop, terminate timer ISR and kill any output
TCCR0 = 0; //turn off timer 0
PORTD.3 = 0; //and kill any leftover pulse
break;
case 'g':
if (mode=='r')
begin
TCNT0=0; //zero the clock
TCCR0=1; //and start it
starting=1; //flag to tell ISR to start new pulse train
end
//putsf("running\r");
break;
case 'p':
//putsf("\r\n#, amp, dur, delay, rep\r") ;
scanf("%d%d%d%d%d", &num, &, &durL, &delayL, &repL) ;
//printf("%d%d%d%d%d\r\n", num,amp,durL,delayL,repL) ;
//convert time units
dur = floor(durL*Conversion+0.5);
delay = floor(delayL*Conversion+0.5);
rep = floor(repL*Conversion);
OCR1A = 256-amp; //loads PWM duty cycle
TCCR1B = 1; //turn on PWM
TCCR1A = 0xc1; //8-bit, inverted PWM mode
TCNT1 = 0; //start at zero to set phase
break;
case 'm':
mode=getchar();
break;
case 'r':
mode=getchar();
break;
end //switch
end //if (USR==1) serial event handler
end //while(1)
end //main
Appendix B. PC Matlab program and GUI screen dump

% Stimulator GUI
%clean up any leftover serial connections
clear all
fclose(instrfind)
%open a window
figure('position',[700 550 280 128],...
'name','Stimulator Control',...
'numbertitle','off')
clf
%open a serial connection
s = serial('COM2',...
'baudrate',9600);
fopen(s)
%define the quit button
x=200; y=0; w=50; h=20;
exit=0;
quitbutton=uicontrol('style','pushbutton',...
'string','Quit', ...
'fontsize',12, ...
'position',[x,y,w,h], ...
'tag','stimcntl',...
'callback','exit=1;close;');
%define the amplitude editable text field
x=10; y=100; w=50; h=20;
ampnow=0;
ampCtl=uicontrol('style','edit',...
'string','100', ...
'fontsize',12, ...
'tag','stimcntl',...
'position',[x,y,w,h],...
'callback',[...
'v=str2num(get(ampCtl,''string''));'...
'if (v<1);set(ampCtl,''string'',num2str(1,''%6.1f''));end;'...
'if (v>100);set(ampCtl,''string'',num2str(100,''%6.1f''));end;'...
'ampnow=1;'...
]);
uicontrol('style','text',...
'string','Amplitude (%)',...
'fontsize',12, ...
'tag','stimcntl',...
'position',[x+w,y,125,h]);
%define the duration editable text field
x=10; y=75; w=50; h=20;
durCtl=uicontrol('style','edit',...
'string','1', ...
'fontsize',12, ...
'tag','stimcntl',...
'position',[x,y,w,h],...
'callback',[...
'v=str2num(get(durCtl,''string''));'...
'if (v<.1);set(durCtl,''string'',num2str(.1,''%6.1f''));end;'...
'if (v>4000);set(durCtl,''string'',num2str(4000,''%6.1f''));end;'...
]);
uicontrol('style','text',...
'string','Duration (mSec)',...
'fontsize',12, ...
'tag','stimcntl',...
'position',[x+w,y,125,h]);
%define the delay editable text field
x=10; y=50; w=50; h=20;
delayCtl=uicontrol('style','edit',...
'string','1', ...
'fontsize',12, ...
'tag','stimcntl',...
'position',[x,y,w,h],...
'callback',[...
'v=str2num(get(delayCtl,''string''));'...
'if (v<.1);set(delayCtl,''string'',num2str(.1,''%6.1f''));end;'...
'if (v>4000);set(delayCtl,''string'',num2str(4000,''%6.1f''));end;'...
]);
uicontrol('style','text',...
'string','Delay (mSec)',...
'fontsize',12, ...
'tag','stimcntl',...
'position',[x+w,y,125,h]);
%define the repeat time editable text field
x=10; y=25; w=50; h=20;
repCtl=uicontrol('style','edit',...
'string','10', ...
'fontsize',12, ...
'tag','stimcntl',...
'position',[x,y,w,h],...
'callback',[...
'v=str2num(get(repCtl,''string''));'...
'if (v<.1);set(repCtl,''string'',num2str(.1,''%6.1f''));end;'...
'if (v>4000);set(repCtl,''string'',num2str(4000,''%6.1f''));end;'...
]);
uicontrol('style','text',...
'string','RepeatTime (mSec)',...
'fontsize',12, ...
'tag','stimcntl',...
'position',[x+w,y,125,h]);
%define the number of pulses editable text field
x=10; y=0; w=50; h=20;
numCtl=uicontrol('style','edit',...
'string','2', ...
'fontsize',12, ...
'tag','stimcntl',...
'position',[x,y,w,h],...
'callback',[...
'v=str2num(get(numCtl,''string''));'...
'if (v<1);set(numCtl,''string'',num2str(1,''%6.1f''));end;'...
'if (v>1000);set(numCtl,''string'',num2str(1000,''%6.1f''));end;'...
]);
uicontrol('style','text',...
'string','Number of pulses',...
'fontsize',12, ...
'tag','stimcntl',...
'position',[x+w,y,125,h]);
%define the manual/repeat radiobuttons
x=200; y=50; w=75; h=20;
radionow=0;
mode='m';
mode1button=uicontrol('style','radiobutton',...
'string','Manual', ...
'fontsize',12, ...
'value',1,...
'position',[x,y,w,h], ...
'tag','stimcntl',...
'callback','radionow=1;');
mode2button=uicontrol('style','radiobutton',...
'string','Repeat', ...
'fontsize',12, ...
'position',[x,y-25,w,h], ...
'tag','stimcntl',...
'callback','radionow=1;');
%define the start train button
x=200; y=100; w=50; h=20;
gonow=0;
quitbutton=uicontrol('style','pushbutton',...
'string','Start', ...
'fontsize',12, ...
'position',[x,y,w,h], ...
'tag','stimcntl',...
'callback','gonow=1;');
%define the stop train button
x=200; y=75; w=50; h=20;
stopnow=0;
quitbutton=uicontrol('style','pushbutton',...
'string','Stop', ...
'fontsize',12, ...
'position',[x,y,w,h], ...
'callback','stopnow=1;');
%Now start handling events
while (exit==0)
%If the start button was pushed
if(gonow)
gonow=0;
dur=num2str(fix(10*str2num(get(durCtl,'string'))));
amp=num2str(fix(1.28*str2num(get(ampCtl,'string'))));
delay=num2str(fix(10*str2num(get(delayCtl,'string'))));
rep=num2str(fix(10*str2num(get(repCtl,'string'))));
num=num2str(fix(str2num(get(numCtl,'string'))));
fprintf(s,['m' mode])
fprintf(s,['p ' num ' ' amp ' ' dur ' ' delay ' ' rep])
fprintf(s,'g')
set(findobj('tag','stimcntl'),'enable','off')
end
%If the amplitude control was changed
if(ampnow)
ampnow=0;
dur=num2str(fix(10*str2num(get(durCtl,'string'))));
amp=num2str(fix(1.28*str2num(get(ampCtl,'string'))));
delay=num2str(fix(10*str2num(get(delayCtl,'string'))));
rep=num2str(fix(10*str2num(get(repCtl,'string'))));
num=num2str(fix(str2num(get(numCtl,'string'))));
fprintf(s,['p ' num ' ' amp ' ' dur ' ' delay ' ' rep])
end
%If the stop button was pushed
if(stopnow)
stopnow=0;
fprintf(s,'s') %sends the stop command
fprintf(s,'mr') %cancels the pushbutton
set(findobj('tag','stimcntl'),'enable','on')
end
%If either of the radio buttons was pushed
if(radionow)
radionow=0;
if (gco==mode1button)
set(mode2button,'value',0)
mode='m';
else
set(mode1button,'value',0)
mode='r';
end
end
drawnow %force a window redrawx
end
%close the serial port
fclose(s)
Appendix C. 8515 Circuit board
A circuit board was built using expressPCB software. The board can be viewed, modified, or ordered using software from expresspcb.com. This version assumes that only the timing output will be used, and not the amplitude control. Note that one capacitor (shown in white) was added after the circuit board was produced.

The modified user interface for this board is shown below. It has no amplitude control.