The core processor in the Geiger counter is a Arduino ATmega328. It has 2kBytes of sdram 32kB of Flash and 2kb of EEPROM. And its running at 16Khz. Consider this carefully.
It means that memory will be completely filled with the following;
* 2000 character string
* 400 word sentence (at a 5 char average per word )
* an array of 500 32 bit integers
It means that excessive overhead and code niceties are often sacrificed for the sake of code speed.
Since it is a waste of more memory to keep a table of malloced and freed areas of memory embedded software is generally designed to using compile time data structures or stack allocated variables rather than heap data.
The Geigers counters code is built in this manor, Its not petty but its functional and efficient. The code is divided into 2 files the header and main code file
The header contains the constants such as which parts of that hardware and wired to what pins.
If you look closely you might not that I made a mistake when i selected the pins for the LCD since A3(Analogue 3) is already in use by the WIZNET(wireless radio) chip.
Another point to note is that is the "state" and "LcdDisplayMode" enums, this clearly implies that the code makes use of at least 1 state machine. In actual fact there are several but more on that later. I often consider stateful code to be the poor-mans threads. Coding this way allows the low spec hardware to quickly juggle a rather large set of real-time tasks without taking too much time for each. All you have to do a a code is segment the code into manageable chunks and allocate a state for each subsection or task.
enum states { RESET = 0, SOFT_RESTART, INIT_DHCP, OBTAIN_IP, INIT_CLIENT, NORMAL, CONNECT, SEND_DATA, RESP_DATA }; typedef enum { LCD_AVE_READING, LCD_INST_READING, LCD_STATE, LCD_UPTIME, LCD_MAX_MODE } LcdDisplayMode; // DEVICE SETTINGS -- REPLACE WITH A PROPER MAC ADDRESS byte macAddress[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xBA, 0xBE }; // DEVICE SETTINGS NetRAD - this is specific to the NetRAD board #define GEIGER_INTERRUPT_PIN 1 // pin of the gieger counter #define PIN_SPKR 6 // pin number of piezo speaker #define PIN_LED 7 // pin number of event LED #define WIZNET_RESET_PIN A1 #define RADIO_SELECT A3 //DEVICE SETTINGS LCD Shield #define BUTTON A0 #define LCD_PINS 9,8,A2,A5,A3,A4 #define URI_MAX 20 #define LAN_ON_MASK 0x80 #define DHCP_ON_MASK 0x40 #define FAIL_COUNT_MASK 0x3f
You will note the "tubes" variable uses the array of structs trick so that I can statically allocate a mix of strings and integers into its initialization. This array provides the factory standard calibration constants for Caesium and the most commonly found Geiger tubes that are compatible with the NetRad shield, as you can see the board was built to be very flexible.
Also note that Arduinos PROGMEM macro keeps this data firmly out of SDRAM and EEPROM memory and in the flash. This makes it necessary to later load the data from the flash when needed but saves the SDRAM from instant overflow and frees up more working memory. I also encountered a problem with putting floats into the flash so dividing the mantissa and exponent become necessary.
#define MAX_TUBE 8 #define CUSTOM_TUBE 1 typedef struct { const char* label; uint32_t convFactorMag; //something breaks if u put floats into PROGMEM.. int8_t convFactorExp; } TubeEntry; // this is ugly but.. dont blame me http://arduino.cc/en/Reference/PROGMEM prog_char tubeLabel1[] PROGMEM = "UNKNOWN"; prog_char tubeLabel2[] PROGMEM = "CUSTOM_TUBE"; prog_char tubeLabel3[] PROGMEM = "LND_712"; prog_char tubeLabel4[] PROGMEM = "SBM_20"; prog_char tubeLabel5[] PROGMEM = "J408GAMMA"; prog_char tubeLabel6[] PROGMEM = "J306BETA"; prog_char tubeLabel7[] PROGMEM = "INSPECTOR"; prog_char tubeLabel8[] PROGMEM = "CRM100"; //floats dont seem to be healthy in PROGMEM either! // 1CPM = ?? uS/hr PROGMEM TubeEntry tubes[MAX_TUBE] = { // label, uSv/hr factor { tubeLabel1, 0, 0 }, // ??? - use nothing! { tubeLabel2, 0, 0 }, // ??? - use conversion factor without caring { tubeLabel3, 2333, -6 }, // 0.002333 - http://www.lndinc.com/products/348/ { tubeLabel4, 57, -4 }, // 0.0057 - http://www.libelium.com/wireless_sensor_networks_to_control_radiation_levels_geiger_counters { tubeLabel5, 0, 0 }, // ??? - North Optic { tubeLabel6, 0, 0 }, // ??? - North Optic { tubeLabel7, 29, -4 }, // 0.0029 - { tubeLabel8, 0, 0 }, // ??? - };
And the definitions of the main memory stores in the system state_t and settings_t.
* state_t is the non-permanent set of operating conditions, it will exist in SDRAM
* settings_t is the permanent data that is loaded and save to the EEPROM. In theory I can make this consume up to 2k however it needs to be loaded into SDRAM when the device is operating, as a result you still need to use it sparingly, or break it into pages.
//static flashed settings typedef struct { //tube/counter settings float conversionCoefficient; // The conversion coefficient from cpm to µSv/h uint32_t readingIntervalMillis; uint8_t tubeModel; uint8_t emaHalfLife; // the number of samples before 50% redution for Expondential average comptation //ether setings uint8_t lanDhcpFails; //bit 1 lanOn, bit 2dhcpOn, bit 3-8 FailedConsCount uint8_t ipAddr[4]; uint8_t gateway[4]; uint8_t subnet[4]; //upload server settings uint8_t uploadIPAddr[4]; char uploadURI[URI_MAX]; } settings_t; // Interrupt mode: // * For most geiger counter modules: FALLING // * Geiger Counter Twig by Seeed Studio: RISING //dynamic runtime settings typedef struct { //execution state uint8_t state; uint8_t lcd_mode; bool lcd_refresh; //lan status uint8_t connFailCnt; uint32_t dhcpMaintainceTime; // The last connection time to disconnect from the server after uploaded feeds uint32_t lastConnectionTime; // The last connection time to disconnect from the server after uploaded feeds //time status uint32_t now; // millisec accumulative counter -- prone to overflow! // uptime status.. uint16_t upTimeMillis; uint8_t upTimeSec; uint8_t upTimeMin; uint8_t upTimeHours; uint8_t upTimeDays; // reading data last 10.. volatile uint8_t eventFlag; // Event flag signals when a geiger event has occurred uint32_t count; // Value to store counts per minute //current event uint32_t lastReading; // millisec accumative counter -- prone to overflow! uint32_t totalCount; float countsPerMinute; float microsievertPerHour; //Exponential moving average float emaCountsPerMinute; float emaMicrosievertPerHour; } state_t;
The Arduino comes with a nice set of headers, the internal unit support is provided by the avr libs. While the LiquidCrystal, Ethernet support the most common of peripheral devices used with by hobbyists.
As you can see the take the static compile time object approach common in embedded software. One note here new and delete don't work in the Arduino compilers so don't even bother trying to use them.
#include <stdint.h> #include <avr/eeprom.h> #include <avr/wdt.h> #include <limits.h> #include <LiquidCrystal.h> #include <SPI.h> #include <Ethernet.h> #include "PrivateSettings.h" // this holds the info for the device static settings_t settings; // holds the control info for the device static state_t state; //Web Client Client client; //LCD LiquidCrystal lcd(LCD_PINS);
One large draw back with the Ardiunos is that printf is missing and the output interfaces tend to be inhomogeneous enough to be frustrating. In the end it annoyed me sufficiently well enough that I ended up writing a facade for the whole mess. It of course adds a slight overhead but in terms debug; the ability to redirect any output to network, serial port or lcd was invaluable.
//Print wrapper system.. cause it sucks without it class PrintCore { public: virtual void endl() const = 0; virtual void clr() const = 0; virtual void p(const unsigned long val) const = 0; virtual void p(const unsigned int val) const = 0; virtual void p(const int val) const = 0; virtual void p(const char* val) const = 0; virtual void p(const char val) const = 0; }; class PrintLCD : public PrintCore { private: mutable int line; public: PrintLCD() : line(0) {} virtual void endl() const { line = (line+1)%2; lcd.setCursor(0,line); } virtual void clr() const { lcd.clear(); line = 0; lcd.setCursor(0,0); } virtual void p(const unsigned long val) const { lcd.print(val,DEC); } virtual void p(const unsigned int val) const { lcd.print(val); } virtual void p(const int val) const { lcd.print(val); } virtual void p(const char* val) const { lcd.print(val); } virtual void p(const char val) const { lcd.print(val); } }; class PrintSerial : public PrintCore { virtual void endl() const { Serial.println(); } virtual void clr() const { } virtual void p(const unsigned long val) const { Serial.print(val,DEC); } virtual void p(const unsigned int val) const { Serial.print(val); } virtual void p(const int val) const { Serial.print(val); } virtual void p(const char* val) const { Serial.print(val); } virtual void p(const char val) const { Serial.print(val); } }; class PrintWebClient : public PrintCore { virtual void endl() const { client.println(); } virtual void clr() const { } virtual void p(const unsigned long val) const { client.print(val,DEC); } virtual void p(const unsigned int val) const { client.print(val); } virtual void p(const int val) const { client.print(val); } virtual void p(const char* val) const { client.print(val); } virtual void p(const char val) const { client.print(val); } }; //main print wrapper.. class PrintClass : public PrintCore { PrintCore* core; PrintLCD lcd_mode; PrintSerial serial_mode; PrintWebClient client_mode; public: PrintClass() { serial(); } //mode void serial() { core = &serial_mode; } void lcd() { core = &lcd_mode; } void client() { core = &client_mode; } //standard virtual void endl() const { core->endl(); } virtual void clr() const { core->clr();} virtual void p(const unsigned long val) const { core->p(val); } virtual void p(const unsigned int val) const { core->p(val); } virtual void p(const int val) const { core->p(val); } virtual void p(const char* val) const { core->p(val); } virtual void p(const char val) const { core->p(val); } //commons void pln(const char* val) const { core->p(val); core->endl(); } void ulng(const unsigned long val) const { core->p(val); } //specials void prog(const prog_char* data) const { //IN FLASH(code) print //WARNING ardindo have to use special functions conserve sram use // this wraps up the print string to use the PSTR flash offload string char c=pgm_read_byte(data++); while(c != 0) { p(c); c=pgm_read_byte(data++); } } void progln(const prog_char* data) const { prog(data); endl(); } void pad(const int val, int padding=-1) const { int mult = 1; if (padding > 0) { //not designed for -ves while(--padding > 0) mult *= 10; while((val < mult) && (mult != 1)) { p('0'); mult /= 10; } } p((int)val); } void flt(double val, byte precision) const { if( precision <= 0) return; // prints val with number of decimal places determine by precision // precision is a number from 0 to 6 indicating the desired decimial places // example: printDouble( 3.1415, 2); // prints 3.14 (two decimal places) //rounding unsigned long mult = 1; //start at 10 for rounding byte padding = precision-1; while(precision--) mult *=10; val += 0.5/mult; //round p(int(val)); //prints the int part p('.'); // print the decimal point unsigned long frac; //get fraction and remove negative if(val >= 0) frac = (val - int(val)) * mult; else frac = (int(val)- val ) * mult; //padding compute unsigned long frac1 = frac; while(frac1 /= 10) padding--; //print padding while(padding--) p('0'); //print faction //p(frac,DEC) ; p(frac) ; } //OK getting highly geiger specific now... // Just a utility function to nicely format an IP address. void ip(const uint8_t* ipAddr) { for(int i = 0;i < 4; i++) { if (i!=0) p('.'); p((int)ipAddr[i]); } } void upTime(bool lng=true) { const char* comma = PSTR(":"); pad(state.upTimeDays); prog(PSTR(" days ")); if(!lng) endl(); pad(state.upTimeHours , 2); prog(comma); pad(state.upTimeMin , 2); prog(comma); pad(state.upTimeSec , 2); prog(comma); if(lng) pad(state.upTimeMillis, 3); } void conversionFactor() { prog(PSTR("ConversionFactor : 1CPM is ")); flt(settings.conversionCoefficient,4); prog(PSTR(" uS/Hr")); if (settings.conversionCoefficient != 0) { prog(PSTR("Hence 1 uS/h is : ")); flt((1.0/settings.conversionCoefficient),4); prog(PSTR(" CPM ")); } } void tubeLabel() { if (settings.tubeModel < MAX_TUBE) { prog(PSTR(" - ")); const char* labelProgmemPtr = (const char*)pgm_read_word(&tubes[settings.tubeModel].label); prog(labelProgmemPtr); } } }; PrintClass print;
The main user interface to the device is the USB based serial port, this interface is how the device is initially configured and calibrated(and its software is uploaded via this also). The next part of the code will handle user input via this method.
The core routine in this was the "cmdPoll" this would simply gather input coming in from the serial port until a enter is struck. The gathered line is then dispatched for processing in the "cmdParse" function, which compares it to another array of strings and function pointers. This then dispatch's it out to the found command function or dumps the help if needed.
Note that the majority of the commands are build as getter/setters. If ran parameter-less they will dump the current setting. But if a parameter is supplied it will first update it and then dump the new setting. It reduces coding effort and cuts down the mess of commands.
/**************************************************************************/ // Set address /**************************************************************************/ void cmdSetLanOn(char *args) { if (args) { if (strtol(args, NULL, 10) != 0) settings.lanDhcpFails |= LAN_ON_MASK; else settings.lanDhcpFails &= ~LAN_ON_MASK; state.state = SOFT_RESTART; } print.prog(PSTR("Lan is set to ")); print.pad((settings.lanDhcpFails & LAN_ON_MASK) != 0); print.progln(PSTR(" Off=0")); } void cmdSetDhcpOn(char *args) { if (args) { if (strtol(args, NULL, 10) != 0) settings.lanDhcpFails |= DHCP_ON_MASK; else settings.lanDhcpFails &= ~DHCP_ON_MASK; state.state = SOFT_RESTART; } print.prog(PSTR("DHCP set to ")); print.pad((settings.lanDhcpFails & DHCP_ON_MASK) != 0); print.progln(PSTR(" Off=0")); } void cmdSetFailsUntilReset(char *args) { if (args) { uint8_t fails = strtol(args, NULL, 10); fails = fails & FAIL_COUNT_MASK; settings.lanDhcpFails = (settings.lanDhcpFails & ~FAIL_COUNT_MASK) | fails; state.state = SOFT_RESTART; } print.prog(PSTR("Max Fails til reset set to ")); print.pad(settings.lanDhcpFails & FAIL_COUNT_MASK); } void readIP(char *args, uint8_t* ipAddr) { if (args) { char* part = strtok(args, "."); for(int i = 0;(i < 4) && (part != NULL); i++) { ipAddr[i] = strtol(part, NULL, 10); part = strtok(NULL, "."); } state.state = SOFT_RESTART; } } void cmdSetIpAddr(char *args) { readIP(args, settings.ipAddr); print.prog(PSTR("IP set to: ")); print.ip(settings.ipAddr); } void cmdSetGateway(char *args) { readIP(args, settings.gateway); print.prog(PSTR("Gateway set to: ")); print.ip(settings.gateway); } void cmdSetSubnet(char *args) { readIP(args, settings.subnet); print.prog(PSTR("Subnet set to: ")); print.ip(settings.subnet); } void cmdSetUploadIpAddr(char *args) { readIP(args, settings.uploadIPAddr); print.prog(PSTR("Upload IP address set to: ")); print.ip(settings.uploadIPAddr); } void cmdSetUploadURI(char *args) { if (args) { uint32_t len = strlen(args); if (len > (URI_MAX-1)) len = (URI_MAX-1); state.state = SOFT_RESTART; memset(settings.uploadURI, 0 , URI_MAX); memcpy(settings.uploadURI, args, strlen(args)); } print.prog(PSTR("Upload URI set to: ")); Serial.println(settings.uploadURI); } /**************************************************************************/ // TUBE Settings /**************************************************************************/ void cmdSetTube(char *args) { if (args) { settings.tubeModel = (uint32_t)strtol(args, NULL, 10); if (settings.tubeModel > MAX_TUBE) settings.tubeModel = 0; if (settings.tubeModel != CUSTOM_TUBE) { //ugly -- but something goes wrong with floats in PROGMEM... uint32_t convFactMag = (pgm_read_word(&tubes[settings.tubeModel].convFactorMag)); int8_t convFactExp = (pgm_read_byte(&tubes[settings.tubeModel].convFactorExp)); float convFactor = convFactMag; if (convFactExp < 0) while (convFactExp != 0) { convFactor /= 10.0; convFactExp++; } else if (convFactExp > 0) while (convFactExp != 0) { convFactor *= 10.0; convFactExp--; } settings.conversionCoefficient = convFactor; } } print.prog(PSTR("Tube model: ")); print.pad(settings.tubeModel); print.tubeLabel(); print.endl(); print.conversionFactor(); } void cmdSetConversionFactor(char *args) { if (args) settings.conversionCoefficient = strtol(args, NULL, 10); print.conversionFactor(); } void cmdSetReadingInterval(char *args) { if (args) settings.readingIntervalMillis = strtol(args, NULL, 10); print.prog(PSTR("Reading Interval set to ")); print.ulng(settings.readingIntervalMillis); print.endl(); } void cmdSetEmaHalfLife(char *args) { if (args) settings.emaHalfLife = strtol(args, NULL, 10); print.prog(PSTR("EMA half life set to ")); print.pad(settings.emaHalfLife); print.endl(); } /**************************************************************************/ // Print out the current device ID /**************************************************************************/ void cmdReading(char *args) { unsigned long deltaMillis = elapsedTime(state.lastReading); float guessCPM = state.count * 60000 / deltaMillis; float guessDose = guessCPM * settings.conversionCoefficient; print.prog (PSTR("UpTime:")); print.upTime(); print.endl(); print.progln(PSTR("Current:")); print.prog (PSTR(" - time : ")); print.ulng(deltaMillis); print.progln(PSTR("msec")); print.prog (PSTR(" - counts : ")); print.pad(state.count); print.progln(PSTR("counts")); print.prog (PSTR(" - guessed : ")); print.flt(guessCPM,4); print.progln(PSTR("CPM")); print.prog (PSTR(" - guessed : ")); print.flt(guessDose,4); print.progln(PSTR("uS/hr")); print.progln(PSTR("Prior:")); print.prog (PSTR(" - reading : ")); print.flt(state.countsPerMinute,4); print.progln(PSTR("CPM")); print.prog (PSTR(" - reading : ")); print.flt(state.microsievertPerHour,4); print.progln(PSTR("uS/hr")); print.progln(PSTR("Average:")); print.prog (PSTR(" - reading : ")); print.flt(state.emaCountsPerMinute,4); print.progln(PSTR("CPM")); print.prog (PSTR(" - reading : ")); print.flt(state.emaMicrosievertPerHour,4); print.progln(PSTR("uS/hr")); } void cmdSettings(char *args) { print.progln(PSTR(" GIGEIR TUBE:")); print.prog (PSTR(" - tube : ")); print.pad(settings.tubeModel); print.tubeLabel(); print.endl(); print.prog (PSTR(" - conv : ")); print.flt(settings.conversionCoefficient,4); print.progln(PSTR(" uS/Hr = 1CPM")); print.prog (PSTR(" - interval: ")); print.ulng(settings.readingIntervalMillis); print.endl(); print.prog (PSTR(" - EMAHalf : ")); print.pad(settings.emaHalfLife); print.endl(); print.progln(PSTR(" Lan:")); print.prog (PSTR(" - lan : ")); print.pad((settings.lanDhcpFails & LAN_ON_MASK ) != 0); print.progln(PSTR(" (0=off)")); print.prog (PSTR(" - dhcp : ")); print.pad((settings.lanDhcpFails & DHCP_ON_MASK) != 0); print.progln(PSTR(" (0=off)")); print.prog (PSTR(" - fail lim: ")); print.pad(settings.lanDhcpFails & FAIL_COUNT_MASK); print.endl(); print.prog (PSTR(" - ip : ")); print.ip(settings.ipAddr); print.endl(); print.prog (PSTR(" - gateway : ")); print.ip(settings.gateway); print.endl(); print.prog (PSTR(" - subnet : ")); print.ip(settings.subnet); print.endl(); print.progln(PSTR(" UPLOAD:")); print.prog (PSTR(" - upip : ")); print.ip(settings.uploadIPAddr); print.endl(); print.prog (PSTR(" - upuri : ")); Serial.println(settings.uploadURI); } void cmdReset(char *args) { state.state = RESET; print.progln(PSTR("RESETING...\n")); } void cmdSave(char *args) { eeprom_write_block((byte *)&settings, 0, sizeof(settings_t)); print.progln(PSTR("Settings saved\n")); } void cmdHelp(char *args) { print.progln(PSTR(" GIGEIR TUBE:")); print.progln(PSTR(" - tube : set the tube type(autoset others)")); print.progln(PSTR(" - conv : set the 1CPM -> uS/Hr conversion")); print.progln(PSTR(" - inter: set the rate of reading compution/upload")); print.progln(PSTR(" - half : set the EMA half life")); print.progln(PSTR(" Lan:")); print.progln(PSTR(" - lan : turn lan on/off")); print.progln(PSTR(" - dhcp : turn dhcp on/off")); print.progln(PSTR(" - ip : set static ip")); print.progln(PSTR(" - fail : number of upload fails before reset")); print.progln(PSTR(" - gate : set static gateway")); print.progln(PSTR(" - snet : set static subnet")); print.progln(PSTR(" UPLOAD:")); print.progln(PSTR(" - upip : set a custom server ip")); print.progln(PSTR(" - upuri: set a custom server URI")); print.progln(PSTR(" ACTIONS:")); print.progln(PSTR(" - read : display a detailed reading")); print.progln(PSTR(" - sett : display the settings")); print.progln(PSTR(" - reset: reset the device")); print.progln(PSTR(" - save : save any changes to settings")); print.progln(PSTR("")); } //MAX cmds are 5 + 20 in args .. Ardiunos dont have masses of space #define CMD_MAX_LENGTH 25 typedef struct { const char* cmd; void (*func)(char*); } CmdEntry; void cmdParse(char* msg) { static CmdEntry cmdTable[] = { { "read", cmdReading }, { "sett", cmdSettings }, { "lan", cmdSetLanOn }, { "dhcp", cmdSetDhcpOn }, { "fail", cmdSetFailsUntilReset }, { "ip", cmdSetIpAddr }, { "gate", cmdSetGateway }, { "snet", cmdSetSubnet }, { "upip", cmdSetUploadIpAddr }, { "upuri", cmdSetUploadURI }, { "tube", cmdSetTube }, { "conv", cmdSetConversionFactor }, { "inter", cmdSetReadingInterval }, { "half", cmdSetEmaHalfLife }, { "reset", cmdReset }, { "save", cmdSave }, { NULL, NULL } }; char* cmd = strtok(msg, " "); char* arg = strtok(NULL, " "); if (cmd == NULL) { cmdHelp(NULL); return; } for (uint32_t i=0; cmdTable[i].cmd != NULL; i++) { if (!strcmp(cmd, cmdTable[i].cmd)) { (*cmdTable[i].func)(arg); return; } } cmdHelp(NULL); } void cmdPoll() { static char msg[CMD_MAX_LENGTH+1]; static uint8_t msg_idx = 0; bool process = false; while (Serial.available()) { if (msg_idx == 0) { print.endl(); print.prog(PSTR("CMD> ")); } char c = Serial.read(); if (c == '\r') { process = true; } else if ( c == '\b') { Serial.print(c); if (msg_idx > 0) msg_idx--; } else if(!process) { if (msg_idx < CMD_MAX_LENGTH) { // normal character entered. add it to the buffer Serial.print(c); msg[msg_idx++] = c; msg[msg_idx] = 0; } } else { //empty serial port... of junk after enter.. } } if (process) { print.endl(); Serial.println(msg); cmdParse(msg); msg_idx = 0; msg[msg_idx] = '\0'; } }
The Geiger is not always connected to a serial port. User input can also come in from the pull up switch added on the LCD shield and wired to the A1 pin. The button at the moment is setup to just toggle the LCD display modes. As you can see the lcd is refreshed in proxy from the button push via lcd_refresh this is so that other areas of the code can also trigger the lcd to change.
void handleButton() { static bool buttonPushState = false; static bool prevButtonPushState = false; // check if the pushbutton is pressed. // if it is, the buttonState is HIGH: buttonPushState = (digitalRead(BUTTON) == HIGH); if (buttonPushState != prevButtonPushState) { prevButtonPushState = buttonPushState; if (buttonPushState == true) { state.lcd_mode = (state.lcd_mode+1)%LCD_MAX_MODE; state.lcd_refresh = true; } } if (state.lcd_refresh) { state.lcd_refresh = false; print.lcd(); print.clr(); switch (state.lcd_mode) { case LCD_AVE_READING: // LCD output print.prog(PSTR("Av:")); print.flt(state.emaCountsPerMinute,1); print.endl(); print.flt(state.emaMicrosievertPerHour,4); break; case LCD_INST_READING: // LCD output print.prog(PSTR("Rw:")); print.flt(state.countsPerMinute,1); print.endl(); print.flt(state.microsievertPerHour,4); break; case LCD_STATE: print.prog(PSTR("St:")); switch (state.state) { case RESET: print.prog(PSTR("RST")); break; case SOFT_RESTART: print.prog(PSTR("SRST")); break; case INIT_DHCP: print.prog(PSTR("DHCP")); break; case OBTAIN_IP: print.prog(PSTR("IP")); break; case INIT_CLIENT: print.prog(PSTR("CLNT")); break; case NORMAL: print.prog(PSTR("NORM")); break; case CONNECT: print.prog(PSTR("CNCT")); break; case SEND_DATA: print.prog(PSTR("SEND")); break; case RESP_DATA: print.prog(PSTR("RESP")); break; default: print.prog(PSTR("????")); break; } print.endl(); print.prog(PSTR("Fails:")); print.pad(state.connFailCnt,2); break; case LCD_UPTIME: print.prog(PSTR("Up:")); print.upTime(false); break; } print.serial(); } }
Additionally for accutate computations of the radiation readings special care needs to be taken to compute the amount of time that has elapased between readings and updates of the clock. Poorly written clocking code will often drift quite dramatically, this destroying your readings accuracy. The key line is the "state.now = millis();" which samples and records the time atomically as possible, The calculations then account for timer overflows and computes the full time delta using the current and prior samples of the clock.
/**************************************************************************/ // calculate elapsed time. this takes into account rollover. /**************************************************************************/ unsigned long elapsedTime(unsigned long startTime) { unsigned long stopTime = millis(); if (startTime >= stopTime) return startTime - stopTime; else return (ULONG_MAX - (startTime - stopTime)); } void updateTime() { unsigned long prevNow = state.now; state.now = millis(); unsigned long deltaMillis; if (state.now >= prevNow) deltaMillis = state.now - prevNow; else deltaMillis = ULONG_MAX - (prevNow - state.now); state.upTimeMillis += deltaMillis; if ( state.upTimeMillis >= 1000) { state.upTimeSec += state.upTimeMillis / 1000; state.upTimeMillis = state.upTimeMillis % 1000; state.lcd_refresh |= (state.lcd_mode == LCD_UPTIME); if (state.upTimeSec >= 60) { state.upTimeMin += state.upTimeSec / 60; state.upTimeSec = state.upTimeSec % 60; if (state.upTimeMin >= 60) { state.upTimeHours += state.upTimeMin / 60; state.upTimeMin = state.upTimeMin % 60; if (state.upTimeHours >= 24) { state.upTimeDays += state.upTimeHours / 24; state.upTimeHours = state.upTimeHours % 24; } } } } }
The final interface on the device is the Ethernet port. Using the port is a bit complex but luckly the Arduino has a Ethernet lib complete with a Web client. As a result all I really need to do is generate a raw HTTP get request to a waiting server with the current raw reading for it to save. And at some point later read back its response.
/************************************************************************** Send data to server **************************************************************************/ void handleClient() { if(state.state == CONNECT) { if (client.connected()) { return; print.prog(PSTR("Disconnecting.")); client.stop(); } print.endl(); // Try to connect to the server print.prog(PSTR("Connecting.")); if (client.connect()) { print.progln(PSTR("Connected.")); state.lastConnectionTime = millis(); // clear the connection fail count if we have at least one successful connection state.connFailCnt = 0; state.state = SEND_DATA; state.lcd_refresh |= (state.lcd_mode == LCD_STATE); } else { state.connFailCnt++; print.prog(PSTR("Failed count:")); print.pad(state.connFailCnt); print.endl(); uint8_t fail_max = settings.lanDhcpFails & FAIL_COUNT_MASK; if((fail_max >0) && (state.connFailCnt > fail_max)) state.state = SOFT_RESTART; else state.state = NORMAL; state.lcd_refresh |= (state.lcd_mode == LCD_STATE); } } else if(state.state == SEND_DATA) { print.prog(PSTR("Upload.")); print.client(); print.prog(PSTR("GET ")); print.p(settings.uploadURI); print.prog(PSTR("?cpm=")); print.flt(state.countsPerMinute,1); print.prog(PSTR("&mSh=")); print.flt(state.microsievertPerHour,4); print.progln(PSTR(" HTTP/1.0")); print.endl(); print.serial(); state.state = RESP_DATA; state.lcd_refresh |= (state.lcd_mode == LCD_STATE); print.progln(PSTR("Uploaded.")); } else if(state.state == RESP_DATA) { //handle data transmission while(client.available()) { // Echo received strings to a host PC char c = client.read(); Serial.print(c); } if (!client.connected()) { Serial.println("disconnected."); client.stop(); state.state = NORMAL; state.lcd_refresh |= (state.lcd_mode == LCD_STATE); } } }
This bring us to the main body of the code. The Arduino's dont have a main per-say. They use 2 functions. The "setup" function is a run once piece of code just after reset. The "loop" function is basically an endless while(1) loop. The Chibi board is setup so that Ardiuno uses "wdt_enable(WDTO_8S)" as a hardware stop watch. If the stop watch runs out the hardware resets and starts fresh(In this case its was set to 8 secs). The later "wdt_reset()" restarts the timer freash for the next iteration of the loop. This way the Geiger can never get stuck in an unexpected state.
Also note the only interrupt that is registered in the system is the onPulse function. This is the most critical piece of the code, it counts the output from the Geiger tube. Note that its not protected from multiple interrupts, this is because the geiger tubes dead time is sufficently long enough(and the code is next to atomic anyway) for the code to complete and be ready for the next event.
The main loop can be summarized as
* handle reset
* handle serial cmds
* handle user feedback of radiative events
* compute new total uptime
* compute new reading if sampling period is over
* handle button input LCD output
* handle network setup
* handle web client communication
/**************************************************************************/ /*! On each falling edge of the Geiger counter's output, increment the counter and signal an event. The event can be used to do things like pulse a buzzer or flash an LED */ /**************************************************************************/ void onPulse() { state.count++; state.eventFlag = 1; } /**************************************************************************/ // main setup and loop... /**************************************************************************/ void setup() { delay(20); // fill in the UART file descriptor with pointer to writer. //fdev_setup_stream (&uartout, uart_putchar, NULL, _FDEV_SETUP_WRITE); // The uart is the standard output device STDOUT. //stdout = &uartout ; // set up the LCD's number of columns and rows: lcd.begin(8, 2); // Print a message to the LCD. lcd.print("Reset!"); Serial.begin(57600); print.progln(PSTR("Reseting..")); // tick to the usb console // put radio in idle state pinMode(RADIO_SELECT, OUTPUT); digitalWrite(RADIO_SELECT, HIGH); // disable radio chip select // reset the Wiznet chip pinMode(WIZNET_RESET_PIN, OUTPUT); digitalWrite(WIZNET_RESET_PIN, HIGH); delay(20); digitalWrite(WIZNET_RESET_PIN, LOW); delay(20); digitalWrite(WIZNET_RESET_PIN, HIGH); // get the device info eeprom_read_block((byte*)&settings, 0, sizeof(settings_t)); // init the control info memset(&state, 0, sizeof(state_t)); // // enable watchdog to allow reset if anything goes wrong wdt_enable(WDTO_8S); // Attach an interrupt to the digital pin and start counting // Note: // Most Arduino boards have two external interrupts: // numbers 0 (on digital pin 2) and 1 (on digital pin 3) attachInterrupt(GEIGER_INTERRUPT_PIN, onPulse, RISING); state.state = SOFT_RESTART; state.lcd_refresh |= (state.lcd_mode == LCD_STATE); state.count = 0; // kick the watch dog wdt_reset(); print.progln(PSTR("Reset..")); // tick to the usb console } void loop() { //polling loop.. very fast.. pfft // kick the dog only if we're not in RESET state. if we're in RESET // we will just let the device gracefully reset via the watchdag time out.. if (state.state != RESET) wdt_reset(); else state.lcd_refresh |= (state.lcd_mode == LCD_STATE); if (state.state == SOFT_RESTART) { state.state = INIT_DHCP; state.lcd_refresh |= (state.lcd_mode == LCD_STATE); } cmdPoll(); // user events -- poll the command line for any input if (state.eventFlag) { // Advertise a geiger event state.eventFlag = 0; // clear the event flag for later use print.prog(PSTR(".")); // tick to the usb console tone(PIN_SPKR, 1000); // beep the piezo speaker digitalWrite(PIN_LED, HIGH); // flash the LED delay(20); digitalWrite(PIN_LED, LOW); noTone(PIN_SPKR); // turn off the speaker pulse } // up time update -- to fast loosing to many millis updateTime(); if (settings.readingIntervalMillis == 0) return; // uS per hour calc and dump... unsigned long deltaMillis = elapsedTime(state.lastReading); if (deltaMillis > settings.readingIntervalMillis) { state.lastReading = millis(); // loop now cut down to about whatever the UPDATE has been changed to // count current cpm run.. state.countsPerMinute = (float)state.count * 60000.0 / (float)deltaMillis; state.count = 0; // Convert from cpm to õSv/h with the pre-defined coefficient state.microsievertPerHour = state.countsPerMinute * settings.conversionCoefficient; //EMA compute float alpha = 2.0/((float)(settings.emaHalfLife+1)); if (state.emaCountsPerMinute == 0) { state.emaCountsPerMinute = state.countsPerMinute; state.emaMicrosievertPerHour = state.microsievertPerHour; } else { state.emaCountsPerMinute = alpha*state.countsPerMinute + (1.0 - alpha)*state.emaCountsPerMinute; state.emaMicrosievertPerHour = alpha*state.microsievertPerHour + (1.0 - alpha)*state.emaMicrosievertPerHour; } print.endl(); print.prog(PSTR("Up: ")); print.upTime(); print.prog(PSTR(" ")); print.flt(state.countsPerMinute,1); print.prog(PSTR("CPM ")); print.flt(state.microsievertPerHour,4); print.prog(PSTR("uS/hr")); print.prog(PSTR(" EMAve: ")); print.flt(state.emaCountsPerMinute,1); print.prog(PSTR("CPM ")); print.flt(state.emaMicrosievertPerHour,4); print.prog(PSTR("uS/hr")); state.lcd_refresh |= (state.lcd_mode == LCD_AVE_READING) || (state.lcd_mode == LCD_INST_READING); } handleButton(); if ((settings.lanDhcpFails & LAN_ON_MASK) != 0) { if ((settings.lanDhcpFails & DHCP_ON_MASK) != 0) { //// boot and handle dhcp //if (state.state == INIT_DHCP) // { // EthernetDHCP.begin(macAddress, 1); // state.state = OBTAIN_IP; // state.lcd_refresh |= (state.lcd_mode == LCD_STATE); // } //else // handleDHCP(); } else { //no dhcp.. straight to client if ((state.state == INIT_DHCP) || (state.state == OBTAIN_IP)) { state.state = INIT_CLIENT; state.lcd_refresh |= (state.lcd_mode == LCD_STATE); //need to fill in these Ethernet.begin(macAddress, settings.ipAddr, settings.gateway, settings.subnet); print.progln(PSTR("Static Ethernet setup.")); // tick to the usb console } } if (state.state == INIT_CLIENT) { //boot and handle web client //client.stop(); client.init(settings.uploadIPAddr, 80); state.state = NORMAL; state.lcd_refresh |= (state.lcd_mode == LCD_STATE); } else if (state.state >= NORMAL) //only 1 per watch dog tick.. hence else if { if (deltaMillis > settings.readingIntervalMillis) { // send data if ready if (state.state == NORMAL) { state.state = CONNECT; state.lcd_refresh |= (state.lcd_mode == LCD_STATE); } } handleClient(); } } }
No comments:
Post a Comment