Close

Neuron Development: v0.4 part 7: Firmware

A project log for NeuroBytes

Build your own nervous system!

zakqwyzakqwy 12/12/2014 at 06:515 Comments

This is important: Zach will never be a skilled programmer. I can bang out half-functional (albeit inefficient and buggy) code when I need to, but it usually involves a lot of cursing and frivolous Google searches. This being my first foray into AVR-C is no exception, so bear with me. Constructive criticism is encouraged.

A few weeks ago I discussed Neuron v04's circuit design and board layout; when I added that project log, I also added a link to the Neuron Github repo which includes both hardware AND software stuff. I've been really bad with revision control for this project, so with any luck publishing the repo will force me to use good practices when updating the firmware. Seriously--the code is pretty much identical to the contents of a folder called 'v04test7' that was buried in a series of Dropbox directories; I only chose it because the mysteriously named 'v04test8' and 'v04test9' had some undocumented bug that I couldn't figure out.

Originally I tried posting the code as-is using the 'code snippet' function, but that doesn't seem to respect tabs (at least in this Ubuntu 14/Firefox 33 combo) which made it pretty hard to understand. As such, I'm going to go through the main runtime firmware here in its entirety, replacing the inline comments with a more in-depth discussion where it seems to make sense. Note that <code> precedes //comments in sections with multiple snippets.


Licensing

/*
Copywrite 2014, Zach Fredin
zachary.fredin@gmail.com

This file is part of Neuron.
Neuron is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Neuron is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Neuron. If not, see <http://www.gnu.org/licenses/>.
*/
GNU GPL v3 blah blah blah

Libraries

#include <avr/io.h>
#include <avr/interrupt.h>
Pretty basic. We need to send signals to the chip through its I/O lines, and we interrupts to work for the inputs.

Variables

int16_t ledGreenfade = 1;
int16_t ledRedfade = 0;
int16_t ledBluefade = 0;
LED brightness variables, scaled from 0 (dimmest) to 100 (brightest). I don't quite recall why I made these 16-bits; probably to fix a pesky error. Also, the green LED is initially set on (but very dim) which isn't too relevant, since these values will be overwritten as soon as the main program starts.
uint8_t debounceValue = 5;
uint8_t exDebounceCount1 = 0;//excitatory 1, which is K1, pin 8, PA5, and PCINT5.
uint8_t exDebounceCount2 = 0;//excitatory 2, which is K3, pin 10, PA3, and PCINT3.
uint8_t exDebounceCount3 = 0;//excitatory 3, which is K5, pin 12, PA1, and PCINT1.
uint8_t exDebounceCount4 = 0;//excitatory 4, which is K6, pin 13, PA0, and PCINT0.
uint8_t exDebounceCount5 = 0;//excitatory 5, which is K2, pin 9, PA4, and PCINT4.
uint8_t inDebounceCount1 = 0;//inhibitory 2, which is K4, pin 11, PA2, and PCINT2.
Debounce counter limit and individual debounce variables for each of the six inputs; I wanted to keep these separate since I didn't want to invalidate quickly occurring pulses on two different inputs (just a single input). Time units are in ms. Yeah, this could probably be an array.
int16_t potentialTotal = 0;
int16_t inGroundState = 0;
int16_t inGroundStatePrevious = 0;
int16_t exGroundState = 0;
int16_t exGroundStatePrevious = 0;
int16_t decayPotential = 0;
These keep track of the current membrane potential of the Neuron. I track the excitatory and inhibitory portions of this value along with a decay multiplier separately, along with previous values for inhibitory and excitatory potential.
uint8_t potentialTimerOverflow = 5;
uint8_t potentialTimerCounter = 0;

Uh.. to be commented later.

uint16_t timer1Overflow = 10;//FAST loop overflow
uint16_t timer2Overflow = 800;//SLOW loop overflow
uint16_t timer2Counter = 0;
Timer variables. timer2 is nested within timer1, and timer1 is tied with the system clock. Since I'm running these (currently) at 8 MHz, timer1 should click over at 800 kHz and timer2 should run at 1 kHz. My 'scope seems to confirm that, although I should probably check again at some point. AVR timers are confusing.
uint8_t inputMagnitude = 70;//amount each input increases/decreases potential
uint8_t inputStatus = 0b00000000;//current input status (read when stuff changes!)
inputMagnitude, as commented, shows how much each excitatory or inhibitory pulse changes the current membrane potential of the Neuron (with +100 assumed to be the action potential threshold). inputStatus--I like to write out bytes when each bit refers to something because it's easy to quickly understand. I suppose that isn't a great way to learn hex. In this case, I'm setting the inputs low for initialization.
uint8_t fireTimerOverflow = 2;//how long should LED pulses last?
uint8_t fireTimerCounter = 2;
uint8_t fireDelayOverflow = 20;//after firing, how long until sending a pulse?
uint8_t fireDelayCounter = 0;
These variables deal with action potential events and are timed in milliseconds (they're part of the timer2 loop). 'Firing' means flashing all LEDs at once at full brightness, and the delay shows how long to wait until sending a pulse down the axon. I'm not sure that's working quite right, because I'm pretty sure my Neurons aren't updating at 50 Hz. Hmmm...

Interrupt Service Routine

ISR(PCINT0_vect) { //interrupt svc routine called when PCINT0 changes state
//note that this is different than the ATtiny45 version
inputStatus = PINA;
}
Any time stuff changes on the input pins (they're covered by PCINT0_vect), I read their current values into the inputStatus variable and return to the current position in the program. Yay interrupts!

LED update function

void updateLEDs(uint16_t Counter, uint16_t Red, uint16_t Green, uint16_t Blue) {
if (Counter >= Green){
PORTB |= (1<<0);//wahoo, bitwise logic! turns
//the green LED on (low)
//for the first part of the
//PWM waveform
}
else {
PORTB &= ~(1<<0);//more bitwise logic. turns
//the green LED off (high)
//for the second part of the
//PWM waveform.
}
if (Counter >= Red){
PORTB |= (1<<1);//see above. this could
//probably be simplified into
//a swanky function of some
//type.
}
else {
PORTB &= ~(1<<1);
}
if (Counter >= Blue){
PORTB |= (1<<2);
}
else {
PORTB &= ~(1<<2);
}
}
So... I needed to run my main program code pretty fast, but I also wanted PWM fading on each LED channel. Also, Neuron color fading isn't really linear across the full range of membrane potential; when the value is below zero the LED fades from green towards blue, and when above zero it fades from green towards red. This function is intended to be called during the fast loop--the first argument (Counter) says where you are in the PWM waveform, while the next three arguments (Red, Green, Blue) says how long each color should be high for each part of that waveform. Everything else is unnecessary comments, unoptimized code, and flipping bits on PORTB (where the LEDs are the three least significant bits).

System Initialization

void SystemInit(void) {
DDRA = 0b01000000; //IO config: PA0-5 in (dendrites), PA6 out (axon)
PORTA = 0b00000000; //Turns off pull-up resistors on dendrites, sets axon low
DDRB = 0b00000111; //IO config: PB0,1,2 out (LEDs), all others in
PORTB = 0b00000111; //Sets PB0,1,2 high to start (LEDs off).
TCCR1B = 0b00000001; // sets up a timer at 1MHz (or base AVR speed)
sei(); //enable all interrupts: same as changing bit 7 of SREG to 1
GIMSK |= (1<<4);//sets the general input mask register's 4th bit to 1
//to activate the PCIE0 interrupt stuff
PCMSK0 = 0b00111111; //sets the six dendrites to active hardware interrupts (0-5)
inputStatus = PINA;
exDebounceCount1 = debounceValue;
exDebounceCount2 = debounceValue;
exDebounceCount3 = debounceValue;
exDebounceCount4 = debounceValue;
exDebounceCount5 = debounceValue;
inDebounceCount1 = debounceValue;

The comments are pretty reasonable here. DDRA/PORTA are header connectors (second most significant bit is the axon), while DDRB/PORTB are the LEDs (well, at least the three least significant bits). OH YEAH, and the LEDS are wired with a common positive terminal, so pushing those bits HIGH turns them OFF. sei(), GIMSK, and PCMSK0, if I remember the datasheet correctly (doubtful), get interrupts working. The debounce variables probably could be set elsewhere but here they shall remain for now.


Main Program: Slow Loop

if (timer2Counter >= timer2Overflow) {

}

I'm covering the main program in sections, starting from the inside out. The timer2 loop runs at 1 kHz.

exGroundStatePrevious = exGroundState;
exGroundState = 0;
if (((inputStatus & 0b00100000) > 0) & (exDebounceCount1 == debounceValue)) {
exGroundState += inputMagnitude;
}
if (((inputStatus & 0b00001000) > 0) & (exDebounceCount2 == debounceValue)) {
exGroundState += inputMagnitude;
}
if (((inputStatus & 0b00000010) > 0) & (exDebounceCount3 == debounceValue)) {
exGroundState += inputMagnitude;
}
if (((inputStatus & 0b00000001) > 0) & (exDebounceCount4 == debounceValue)) {
exGroundState += inputMagnitude;
}
if (((inputStatus & 0b00010000) > 0) & (exDebounceCount5 == debounceValue)) {
exGroundState += inputMagnitude;
}
inGroundStatePrevious = inGroundState;
inGroundState = 0;
if (((inputStatus & 0b00000100) > 0) & (inDebounceCount1 == debounceValue)) {
inGroundState -= inputMagnitude;
}

This is going to sound horrible, but sometimes I like writing repetitive code. As in, man... I just figured something out and I need to do it six times, time to copy-paste and change a few characters! Makes it look like I got a lot done [shudder].

I start by storing the current excitatory or inhibitory membrane potential value in the appropriate 'previous' variable. Remember--I know what inputStatus is, I get it as soon as anything changes via the interrupt service routine. Each of those IF statements just masks that value against a few bytes (again, spelled out so I understand 'em) to see when various inputs are high. If they're high, and their debounce counters are full, I modify the appropriate variable based on the inputMagnitude value.

Oh man, I just remembered why I did it this way, and why they're called 'ground state'. This is the logic that allows me to use Exciters--you know, the shorting jumpers that allow me to hold a Neuron at a given potential value. Essentially, I can 'sensitize' Neurons using an Exciter so their new ground state is 70 instead of 0, meaning they'll hit an action potential with a single excitatory pulse. Okay, I should probably revise this post down the road to make it more clear--if you're reading this, I apologize for not understanding my own code and documentation.

Also just realized that I never reset the debounce counters, so... uh... they always stay full. Hmm, I guess they aren't as necessary as I thought they might be? Next section!

if (exGroundState < exGroundStatePrevious) {
decayPotential += exGroundStatePrevious - exGroundState;
}
if (inGroundState > inGroundStatePrevious) {
decayPotential -= inGroundState - inGroundStatePrevious; 
Looks to see if excitatory or inhibitory inputs have been REMOVED--as in, exGroundState gets SMALLER or inGroundState gets BIGGER. If either happens, we toss the difference into the decayPotential variable so it can start fading.
if (potentialTimerCounter >= potentialTimerOverflow) {
decayPotential = (decayPotential * 95) / 100;
}
Fading using integer math to multiply by 0.95 every clock cycle. Pretty basic; once it gets low enough it rounds to 0. I think? Hmm, now the spreadsheet is telling me that it might stop at 10. I should look in to that, I suppose. It seems to be working in hardware.
if (exDebounceCount1 < debounceValue) {
exDebounceCount1++;
}
if (exDebounceCount2 < debounceValue) {
exDebounceCount2++;
}
if (exDebounceCount3 < debounceValue) {
exDebounceCount3++;
}
if (exDebounceCount4 < debounceValue) {
exDebounceCount4++;
}
if (exDebounceCount5 < debounceValue) {
exDebounceCount5++;
}
if (inDebounceCount1 < debounceValue) {
inDebounceCount1++;
}
If I'm not mistaken, all of this code is pretty much useless--I never reset the debounce counters so none of these statements should ever resolve true. Hmmph. Something to play around with.
if (fireTimerCounter < fireTimerOverflow) {
fireTimerCounter++;
}
if (fireDelayCounter < fireDelayOverflow) {
fireDelayCounter++;
}
Fire timer stuff, relevant later on. Not much to say now.
potentialTimerCounter++;
timer2Counter = 0;//reset SLOW loop
Iterate timers and reset the loop!

Main Program: Fast Loop

Okay, so I think I've made the assumption for a long time that this stuff happens really fast because it's inside this:

if (TCNT1 >= timer1Overflow) {

}
TCNT1 is the main timer, currently set to the AVR clock rate. BUT HERE'S THE THING--if timer1Overflow is only 10, and I've got more than 10 lines of code (or 10 clock cycles, which could probably be 1 line of crappy code) between the brackets, it will SLOW DOWN to the rate of program execution! I think that's why I've gotten thrown off by timing crap--my loop speed is getting stretched by a bunch of code. Which is shown in this section.
potentialTotal = decayPotential + inGroundState + exGroundState;
if (potentialTotal >= 100) {
fireTimerCounter = 0;
decayPotential -= inputMagnitude * 4;
}
We sum potentialTotal a lot, as it's in the fast loop. Also, this includes firing logic--if the total potential exceeds 100, firing happens and the decay plummets like crazy (to -280 with the current inputMagnitude value). I guess that's why changing the inputMagnitude value will affect sensitivity but NOT refresh rate. More on that later.
if ((potentialTotal == 0) & (fireTimerCounter == fireTimerOverflow)) {
ledRedfade = 0;
ledGreenfade = 100;
ledBluefade = 0;
}
if ((potentialTotal > 0) & (fireTimerCounter == fireTimerOverflow)) {
ledRedfade = potentialTotal;
ledGreenfade = 100 - potentialTotal;
ledBluefade = 0;
}
if ((potentialTotal < 0) & (potentialTotal >= -100) & (fireTimerCounter == fireTimerOverflow)) {
ledRedfade = 0;
ledGreenfade = 100 - (-potentialTotal);
ledBluefade = -potentialTotal;
}
if ((potentialTotal < -100) & (fireTimerCounter == fireTimerOverflow)){
ledRedfade = 0;
ledGreenfade = 0;
ledBluefade = 100;
}
if (fireTimerCounter < fireTimerOverflow) {
ledRedfade = 800;
ledGreenfade = 800;
ledBluefade = 800;
fireDelayCounter = 0;
}
Remember how I said that Neuron LED colors fade in a non-linear fashion? Yup, this covers that logic. Green at 0, fades to Red approaching the action potential, fades to Blue during the refractory period, stays Blue when it's REALLY far into the refractory period, and fires like crazy when the fireTimerCounter drops.

Okay, this is a little trick. An earlier iteration was too bright--everything was indexed to full LED brightness. When I bumped the clock frequency up to 8 MHz from 1 MHz, I didn't change the 0-100 scale of LED brightness (or anything else), but I DID change the slow loop update frequency--as such, most of the time the LEDs run on a 12.5% duty cycle and no longer hurt your eyes when you're staring at a table full of them. However, I wanted flashes to be bright--now they stand out a lot.

if ((fireDelayCounter < fireDelayOverflow) & (fireDelayCounter > fireDelayOverflow / 2)) {
PORTA |= (1<<6);
}
if (fireDelayCounter == fireDelayOverflow) {
PORTA &= ~(1<<6);
}
What happens when a Neuron fires? BAM! ACTION POTENTIAL! SEND A SIGNAL DOWN THE AXON! AKA, flip the second most significant bit of PORTA after a suitable delay.
updateLEDs(timer2Counter, ledRedfade, ledGreenfade, ledBluefade);
timer2Counter++; //increment SLOW loop
TCNT1 = 0; //reset FAST loop
Run the LED update function (it runs a lot!), increment the slow loop counter, and reset the fast counter.

IF YOU MADE IT THIS FAR:

I'm sorry, you've been on quite a journey. I couldn't sleep--for some reason, I had to drag myself out of bed to read through and document some code that I haven't take a critical look at in some time. This a great demonstration of the futility of documentation--even reading through my comments, it took me some time to grasp a few basic concepts about how the program operates, such as the ground potential functions.

Again--constructive criticism is encouraged. I've figured a few things out tonight:

More to come--next time featuring videos of blinky Neurons!

Discussions

Casual Cyborg wrote 04/07/2015 at 14:43 point

Here's my question. I was thinking of taking your design, and sticking a neural network program in there, a simple one, and then instead of your board being a single neuron, it's a node. Did y'all ever consider doing that?

  Are you sure? yes | no

zakqwy wrote 04/07/2015 at 15:54 point

Increasing the computational complexity of individual elements is something we've considered at various points; however, one of the main goals of this project is to allow easy physical reconfiguration of a network at the individual neuron level. It's important to us to ensure that each neuron is a realistic representation of actual biological elements; at this point, our priority is neuroscience education rather than artificial neural network research. If we end up putting a more complex structure in a single chip, we'd likely do it as a support system for a sensory or motor element.

Make sense? 

  Are you sure? yes | no

zakqwy wrote 04/07/2015 at 15:58 point

I should clarify a bit--down the road, we might try putting multi-neuron networks on a single device if it improves the overall educational value of the system (and doesn't hurt the economics--we want to make sure the tool is accessible to a wide range of people). This is definitely a 'future planning' type deal though; for now, we're focused on basic neuroscience education involving individual neurons and simple circuits. Having said that, I definitely encourage you to try playing around with our design as it currently stands!

  Are you sure? yes | no

Casual Cyborg wrote 04/07/2015 at 05:45 point

If I'm not mistaken, is each board and microchip a SINGLE neuron?

  Are you sure? yes | no

zakqwy wrote 04/07/2015 at 13:27 point

Correct! Economically scaling this system to hundreds (or more) of neurons isn't a priority; rather, we're focusing on physical modularity and visualization of neural signals.

  Are you sure? yes | no