Über Compact Fermentation Monitor - Arduino Based

Homebrew Talk - Beer, Wine, Mead, & Cider Brewing Discussion Forum

Help Support Homebrew Talk - Beer, Wine, Mead, & Cider Brewing Discussion Forum:

This site may earn a commission from merchant affiliate links, including eBay, Amazon, and others.

ChemE

Well-Known Member
Joined
Mar 3, 2008
Messages
596
Reaction score
20
Location
Columbia, SC
Xively feed from this code These charts might not be recording data at any given moment as I test new code

Earlier this year I became interested in FuzzeWuzze's excellent guide to a Real Time Online Fermentation Temperature Monitor for ~$60 which I bought the parts for and implemented. However, I noticed that once I got the code working which really wasn't doing all that much, it was consuming a very large portion of the space on the microcontroller. How much space you might ask? 21,244 bytes out of 32,256 for the sketch size (code) and 740 bytes out of 2,040 of SRAM. So the code which took two temperature readings and pushed the data up to Xively (Cosm back then) was taking up 66% of my microprocessor. A very big part of this code footprint was due to the inclusion of several libraries which make these Arduinos very easy to get started with namely OneWire, DallasTemperature, SPI, and Ethernet.

I then became interested in compacting my code and lifting out only those parts of the libraries mentioned above that were required to make the project work. I wound up learning how the OneWire protocol works (you are just turning a pin high and low in a timed sequence in order to send ones and zeros down the line), how to talk directly to the DS18B20s that we use, and how to talk directly to the W5100 that runs the ethernet shield. My code is certainly not basic although it is decently commented nor is it compact; the code itself is 450 lines. What it is however is extraordinarily compact when compiled. The code below takes only 2,444 bytes of sketch size; a reduction of 88.5% as compared to the original easy code and 118 bytes of SRAM; a reduction of 84% as compared to the easy code. This leaves well over 90% of the microprocessor's memory free for other cool things you might want to do in a project. This won't be for everyone but for anyone who is starting to run out of space on their Arduino it should be a breath of fresh air.

Code:
// This sketch assumes two DS18B20s are on pin 3 and are not being operated in parasite mode.  Place your sensor ROMS on
// lines 14 and 15 and if your sensors are plugged into a different pin, adjust line 8 to match the pin you use
//
// ======================================================= Includes =======================================================
#include <util/delay.h>    // Using _delay_ms(#) rather than delay(#) saves 146 bytes of sketch size!

// ========================================= User-Specific Definitions ========================================== //
#define Pin PD3    // Must choose a pin on port D (Digital pins 2-7)
#define Sensor1 "Carboy_1_Temperature"
#define Sensor2 "Ambient_Temperature"
#define FEEDID "102607"
#define APIKEY "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
#define USERAGENT "Test"
const uint8_t Sensor[2][8] =  {{ 0x28, 0xE0, 0xF7, 0x83, 0x04, 0x00, 0x00, 0x0F },      // Thermowell sensor
                                { 0x28, 0x68, 0xEF, 0x7D, 0x04, 0x00, 0x00, 0x62 }};    // Ambient room air sensor
#define MYDELAY 5000-67  // Code execution takes 67 milliseconds
#define DEBUG  0  // Controls the inclusion or exclusion of serial debug information; 0 for no serial output 1 for serial output
#if DEBUG
  unsigned long startClock, endClock;
#endif

// ===================================================== OneWire Definitions ====================================================== 
#define   DIRECT_MODE_OUTPUT    DDRD |= _BV(Pin)
#define   DIRECT_MODE_INPUT     DDRD &= ~_BV(Pin)
#define   DIRECT_WRITE_HIGH     PORTD |= _BV(Pin)
#define   DIRECT_WRITE_LOW      PORTD &= ~_BV(Pin)
#define   DIRECT_READ           PIND & _BV(Pin)

#define STARTCONVO      0x44  // Tells device to take a temperature reading and put it on the scratchpad
#define READSCRATCH     0xBE  // Read EEPROM
#define SKIPROM         0xCC
#define CHOOSEROM       0x55
#define TEMP_LSB        0
#define TEMP_MSB        1

// ================================================= W5100 Definitions =====================================================================
// AVRJazz Mega328 SPI I/O
#define SPI_PORT PORTB
#define SPI_DDR  DDRB
#define SPI_CS   PORTB2

// Wiznet W5100 Op Code
#define WIZNET_WRITE_OPCODE 0xF0
#define WIZNET_READ_OPCODE 0x0F

// Wiznet W5100 Register Addresses
#define MR         0x0000      // Mode Register
#define GAR        0x0001      // Gateway Address: 0x0001 to 0x0004
#define SUBR       0x0005      // Subnet mask Address: 0x0005 to 0x0008
#define SAR        0x0009      // Source Hardware Address (MAC): 0x0009 to 0x000E
#define SIPR       0x000F      // Source IP Address: 0x000F to 0x0012
#define RMSR       0x001A      // RX Memory Size Register
#define TMSR       0x001B      // TX Memory Size Register

#define S0_MR	   0x0400      // Socket 0: Mode Register Address
#define S0_CR	   0x0401      // Socket 0: Command Register Address
#define S0_IR	   0x0402      // Socket 0: Interrupt Register Address
#define S0_SR	   0x0403      // Socket 0: Status Register Address
#define S0_DIPR    0x040C      // Socket 0: Destination IP Address: 0x040C to 0x040F
#define S0_DPORT   0x0410      // Socket 0: Destination Port: 0x0410 to 0x0411
#define S0_PORT    0x0404      // Socket 0: Source Port: 0x0404 to 0x0405
#define SO_TX_FSR  0x0420      // Socket 0: Tx Free Size Register: 0x0420 to 0x0421
#define S0_TX_RD   0x0422      // Socket 0: Tx Read Pointer Register: 0x0422 to 0x0423
#define S0_TX_WR   0x0424      // Socket 0: Tx Write Pointer Register: 0x0424 to 0x0425
#define S0_RX_RSR  0x0426      // Socket 0: Rx Received Size Pointer Register: 0x0425 to 0x0427
#define S0_RX_RD   0x0428      // Socket 0: Rx Read Pointer: 0x0428 to 0x0429

#define TXBUFADDR  0x4000      // W5100 Send Buffer Base Address
#define RXBUFADDR  0x6000      // W5100 Read Buffer Base Address

// S0_MR values
#define MR_CLOSE	  0x00    // Unused socket
#define MR_TCP		  0x01    // TCP

// S0_CR values
#define CR_OPEN          0x01	  // Initialize or open socket
#define CR_CONNECT       0x04	  // Send connection request in tcp mode(Client mode)
#define CR_DISCON        0x08	  // Send closing reqeuset in tcp mode
#define CR_CLOSE         0x10	  // Close socket
#define CR_SEND          0x20	  // Update Tx memory pointer and send data

// S0_SR values
#define SOCK_INIT        0x13	  // Init state
#define SOCK_ESTABLISHED 0x17	  // Success to connect

#define TX_BUF_MASK      0x07FF   // Tx 2K Buffer Mask:
#define RX_BUF_MASK      0x07FF   // Rx 2K Buffer Mask:
#define NET_MEMALLOC     0x05     // Use 2K of Tx/Rx Buffer

#define TCP_PORT         80       // TCP/IP Port

#define MAX_BUF 210

// =================================================================== Begin W5100 Functions ===================================================================
static inline void SPI_Write(uint16_t addr,uint8_t data) {
  SPI_PORT &= ~(1<<SPI_CS);      // Activate the CS pin
  SPDR = WIZNET_WRITE_OPCODE;    // Start Wiznet W5100 Write OpCode transmission
  while(!(SPSR & (1<<SPIF)));    // Wait for transmission complete
  SPDR = (addr & 0xFF00) >> 8;   // Start Wiznet W5100 Address High Bytes transmission
  while(!(SPSR & (1<<SPIF)));    // Wait for transmission complete
  SPDR = addr & 0x00FF;          // Start Wiznet W5100 Address Low Bytes transmission
  while(!(SPSR & (1<<SPIF)));    // Wait for transmission complete
  SPDR = data;                   // Start Data transmission
  while(!(SPSR & (1<<SPIF)));    // Wait for transmission complete
  SPI_PORT |= (1<<SPI_CS);       // CS pin is not active
}

static inline unsigned char SPI_Read(uint16_t addr) {
  SPI_PORT &= ~(1<<SPI_CS);      // Activate the CS pin
  SPDR = WIZNET_READ_OPCODE;     // Start Wiznet W5100 Read OpCode transmission
  while(!(SPSR & (1<<SPIF)));    // Wait for transmission complete
  SPDR = (addr & 0xFF00) >> 8;   // Start Wiznet W5100 Address High Bytes transmission
  while(!(SPSR & (1<<SPIF)));    // Wait for transmission complete
  SPDR = addr & 0x00FF;          // Start Wiznet W5100 Address Low Bytes transmission
  while(!(SPSR & (1<<SPIF)));    // Wait for transmission complete
  SPDR = 0x00;                   // Send Dummy transmission for reading the data
  while(!(SPSR & (1<<SPIF)));    // Wait for transmission complete
  SPI_PORT |= (1<<SPI_CS);       // CS pin is not active
  return(SPDR);
}

static inline void W5100_Init(void)
{
  // Ethernet Setup
  const unsigned char mac_addr[] = {0x00, 0x1D, 0x0D, 0x2C, 0x55, 0x3D};
  const unsigned char ip_addr[] = {192,168,11,8};
  const unsigned char sub_mask[] = {255,255,255,0};
  const unsigned char gtw_addr[] = {192,168,11,1};
  const unsigned char cosm_addr[] = {216,52,233,121};

  // Setting the Wiznet W5100 Mode Register: 0x0000
  SPI_Write(MR,0x80);            // MR = 0b10000000;

  // Setting the Wiznet W5100 Gateway Address (GAR): 0x0001 to 0x0004
  SPI_Write(GAR + 0,gtw_addr[0]);
  SPI_Write(GAR + 1,gtw_addr[1]);
  SPI_Write(GAR + 2,gtw_addr[2]);
  SPI_Write(GAR + 3,gtw_addr[3]);

  // Setting the Wiznet W5100 Source Address Register (SAR): 0x0009 to 0x000E
  SPI_Write(SAR + 0,mac_addr[0]);
  SPI_Write(SAR + 1,mac_addr[1]);
  SPI_Write(SAR + 2,mac_addr[2]);
  SPI_Write(SAR + 3,mac_addr[3]);
  SPI_Write(SAR + 4,mac_addr[4]);
  SPI_Write(SAR + 5,mac_addr[5]);

  // Setting the Wiznet W5100 Sub Mask Address (SUBR): 0x0005 to 0x0008
  SPI_Write(SUBR + 0,sub_mask[0]);
  SPI_Write(SUBR + 1,sub_mask[1]);
  SPI_Write(SUBR + 2,sub_mask[2]);
  SPI_Write(SUBR + 3,sub_mask[3]);

  // Setting the Wiznet W5100 IP Address (SIPR): 0x000F to 0x0012
  SPI_Write(SIPR + 0,ip_addr[0]);
  SPI_Write(SIPR + 1,ip_addr[1]);
  SPI_Write(SIPR + 2,ip_addr[2]);
  SPI_Write(SIPR + 3,ip_addr[3]);    

  // Setting the Wiznet W5100 RX and TX Memory Size (2KB),
  SPI_Write(RMSR,NET_MEMALLOC);
  SPI_Write(TMSR,NET_MEMALLOC);
  
  // Setting the Wiznet to connect to the Cosm server port 80
  SPI_Write(S0_DIPR + 0,cosm_addr[0]);
  SPI_Write(S0_DIPR + 1,cosm_addr[1]);
  SPI_Write(S0_DIPR + 2,cosm_addr[2]);
  SPI_Write(S0_DIPR + 3,cosm_addr[3]);
  SPI_Write(S0_DPORT,((TCP_PORT & 0xFF00) >> 8 ));
  SPI_Write(S0_DPORT + 1,(TCP_PORT & 0x00FF));
  SPI_Write(S0_PORT,((1 & 0xFF00) >> 8 ));    // Set the source port to 1
  SPI_Write(S0_PORT + 1,(1 & 0x00FF));        // Set the source port to 1
  
  // Set the Wiznet to use TCP
  SPI_Write(S0_MR,MR_TCP);                    // Set the mode to TCP  
}

static inline void Connect(void) {
  SPI_Write(S0_CR,CR_OPEN);
  while(SPI_Read(S0_CR));
    uint16_t retries=1000;
    while(SPI_Read(S0_SR)!=SOCK_INIT) {;
      // Could get stuck here forever!!! ================================================================================================================================
      // Break out and cancel data point upload if we spend more than 1 second waiting for the sock to initialize
      if(!--retries) return;
      _delay_ms(1);
    }
        
    SPI_Write(S0_CR,CR_CLOSE);      // Send Close Command
    while(SPI_Read(S0_CR));         // Waiting until the S0_CR is clear
    SPI_Write(S0_MR,MR_TCP);
    SPI_Write(S0_CR,CR_CONNECT);    // Open Socket
    while(SPI_Read(S0_CR));         // Wait for Opening Process
}

static inline uint16_t send(const uint8_t *buf,uint16_t buflen) {
    uint16_t ptr,offaddr,realaddr,txsize,timeout;   

    if (buflen <= 0) return 0;

    // Make sure the TX Free Size Register is available
    txsize=SPI_Read(SO_TX_FSR);
    txsize=(((txsize & 0x00FF) << 8 ) + SPI_Read(SO_TX_FSR + 1));

    timeout=0;
    while (txsize < buflen) {
      _delay_ms(1);

     txsize=SPI_Read(SO_TX_FSR);
     txsize=(((txsize & 0x00FF) << 8 ) + SPI_Read(SO_TX_FSR + 1));

     // Timeout for approx 1000 ms
     if (timeout++ > 1000) {

       // Disconnect the connection
       SPI_Write(S0_CR,CR_DISCON);  // Send Disconnect Command
       while(SPI_Read(S0_CR));      // Wait for Disconecting Process
       return 0;
     }
   }	

   // Read the Tx Write Pointer
   ptr = SPI_Read(S0_TX_WR);
   offaddr = (((ptr & 0x00FF) << 8 ) + SPI_Read(S0_TX_WR + 1));
	
    while(buflen) {
      buflen--;
      // Calculate the real W5100 physical Tx Buffer Address
      realaddr = TXBUFADDR + (offaddr & TX_BUF_MASK);

      // Copy the application data to the W5100 Tx Buffer
      SPI_Write(realaddr,*buf);
      offaddr++;
      buf++;
    }

    // Increase the S0_TX_WR value, so it point to the next transmit
    SPI_Write(S0_TX_WR,(offaddr & 0xFF00) >> 8 );
    SPI_Write(S0_TX_WR + 1,(offaddr & 0x00FF));	

    // Now Send the SEND command
    SPI_Write(S0_CR,CR_SEND);

    // Wait for Sending Process
    while(SPI_Read(S0_CR));	

    return 1;
}

// =================================================================== Begin OneWire Functions ===================================================================
static inline uint8_t read() {
  uint8_t r=0;
      
  noInterrupts();
  for (uint8_t bitMask = 0x01; bitMask; bitMask <<= 1) {
    DIRECT_MODE_OUTPUT;
    DIRECT_WRITE_LOW;
    _delay_us(3);
    DIRECT_MODE_INPUT;
    _delay_us(10);
    if (DIRECT_READ) r |= bitMask;
    _delay_us(53);
  }
  interrupts();
  return r;
}

static inline void write(uint8_t v) {
  noInterrupts();
  for (uint8_t bitMask = 0x01; bitMask; bitMask <<= 1) {
    DIRECT_WRITE_LOW;
    DIRECT_MODE_OUTPUT;
    if (bitMask & v) {	
       _delay_us(10);
       DIRECT_WRITE_HIGH;
      _delay_us(55);
    } else {
      _delay_us(65);
      DIRECT_WRITE_HIGH;
      _delay_us(5);
    }
  }
  DIRECT_MODE_INPUT;
  DIRECT_WRITE_LOW;
  interrupts();
}

static inline void reset(void) {
  uint8_t retries = 125;
  
  noInterrupts();
  DIRECT_MODE_INPUT;
  
  // wait until the wire is high... just in case
  while (!DIRECT_READ) {
    if (--retries == 0) break;
    _delay_us(2);
  }
  
  DIRECT_WRITE_LOW;
  DIRECT_MODE_OUTPUT;
  _delay_us(500);
  DIRECT_MODE_INPUT;
  _delay_us(80);
  retries = !DIRECT_READ;
  interrupts();
  _delay_us(420);
}
// =================================================================== End OneWire Functions ===================================================================

// =================================================================== Begin DS18B20 Functions ===================================================================
static inline int16_t ReadSensor(const uint8_t sensorIndex) {
  uint8_t scratchPad[2];
  reset();
  write(0x55);           // Choose ROM
  for(uint8_t i = 0; i < 8; i++) write(Sensor[sensorIndex][i]);
  write(READSCRATCH);
  scratchPad[TEMP_LSB] = read();
  scratchPad[TEMP_MSB] = read();
  
  return (((int16_t)scratchPad[TEMP_MSB]) << 8) | scratchPad[TEMP_LSB];
}

// Converts the raw temperature from a DS18B20 directly to a string containing the temperature in °F with 4 decimal places
// avoids unnecessary floating point math, float variables, and casts
// TODO: May not work properly with temperatures below 32°F
static inline void Tiny_RawTempToString(uint32_t raw, char * buffer) {  
  uint8_t decimalPos=2;  // default case of a temp between 0 and 99.9
  uint8_t nullPos=7;     // default case of a temp between 0 and 99.9
  
  raw=raw*1125ul+320000ul;    // Hold the temperature
  
  if(raw>1000000ul) {  // We're looking at a positive number with three digits
    decimalPos=3;
    nullPos=8;
  }
  
  for(int8_t i=nullPos;i>-1;i--) {
    if (i==decimalPos) {
      buffer[i]='.';
    }
    else if (i==nullPos) {
      buffer[i]='\0';
    }
    else {
      buffer[i]=raw%10 + '0';
      raw/=10;
    }
  } 
}
// =================================================================== End DS18B20 Functions ===================================================================


// Super cheater itoa that only accepts 0-255; only appropriate for the length of the csv data
void tiny_itoa(uint8_t inp, char * buf) {
  if (inp>=0 && inp<=9) {
    buf[0]=inp + '0';
    buf[1]='\0';
  }
  else if (inp<100) {
    buf[1]=inp%10 + '0';
    inp/=10;
    buf[0]=inp%10 + '0';
    buf[2]='\0';
  }
}

static inline void BuildPut(uint16_t T1, uint16_t T2, uint8_t * buf) {
  // Convert raw temperatures into strings
  char Temp1[8];
  char Temp2[8];
  Tiny_RawTempToString(T1, (char *)Temp1);
  Tiny_RawTempToString(T2, (char *)Temp2);
  
  // Set up data buffer to hold the csv data
  char * dataBuffer[strlen(Sensor1)+strlen(Temp1)+strlen(Sensor2)+strlen(Temp2)+4];
 
  strcpy((char *)dataBuffer,Sensor1);
  strcat_P((char *)dataBuffer,PSTR(","));
  strcat((char *)dataBuffer,Temp1);
  strcat_P((char *)dataBuffer,PSTR("\n"));
  strcat((char *)dataBuffer,Sensor2);
  strcat_P((char *)dataBuffer,PSTR(","));
  strcat((char *)dataBuffer,Temp2);
  strcat_P((char *)dataBuffer,PSTR("\n"));
  
  // Convert the length of the data buffer into a string
  char dataLength[2];
  tiny_itoa(strlen((char *)dataBuffer)-1,(char *)dataLength);
  
  // Construct the HTTP Put
  strcpy_P((char *)buf,PSTR("PUT /v2/feeds/"));
  strcat((char *)buf,FEEDID);
  strcat_P((char *)buf,PSTR(".csv HTTP/1.1\nHost: api.cosm.com\n"));
  strcat_P((char *)buf,PSTR("X-ApiKey: "));
  strcat((char *)buf,APIKEY);
  strcat_P((char *)buf,PSTR("\nUser-Agent: "));
  strcat((char *)buf,USERAGENT);
  strcat_P((char *)buf,PSTR("\nContent-Length: "));
  strcat((char *)buf,dataLength);
  strcat_P((char *)buf,PSTR("\n\n"));
  strcat((char *)buf,(char *)dataBuffer);
}

int main() {
  uint8_t buf[MAX_BUF];  // The character buffer in which the HTTP Put will be constructed
  
  // This code will only run once, after each powerup or reset of the board
  
  // Set pin 3 to input
  //pinMode(pin, INPUT);        // This takes 136 bytes of sketch size
  //DDRD = DDRD | B11110100;    // This does the same thing and only takes 4 bytes of sketch size
                                // This does the same thing since pin 3 is set to input by default
  
  SPI_DDR = (1<<PORTB3)|(1<<PORTB5)|(1<<PORTB2);  // Set MOSI (PORTB3),SCK (PORTB5) and PORTB2 (SS) as output, others as input  
  SPI_PORT |= (1<<SPI_CS);                        // CS pin is not active
  SPCR = (1<<SPE)|(1<<MSTR);                      // Enable SPI, Master Mode 0
  
  #if DEBUG
    init();
    Serial.begin(9600);
    _delay_ms(100); 
  #endif
  
  W5100_Init();

  for (;;) {
    // Request a temperature reading from all Dallas sensors on the OneWire bus, takes 750ms so do it right before sleeping
    reset();
    write(SKIPROM);
    write(STARTCONVO);
    
    Connect();// Open a socket to the Cosm server, takes around 300ms to establish so do it right before sleeping
    _delay_ms(MYDELAY);  // Delay right now, eventually sleep
    send(buf,strlen((char *)buf));// Send the previously constructed HTTP put
    
    // Read the temperatures previously requested
    int16_t rawTemperature1 = ReadSensor(0);  // Read the first sensor
    int16_t rawTemperature2 = ReadSensor(1);  // Read the second sensor
    BuildPut(rawTemperature1, rawTemperature2, buf);
    
    #if DEBUG
      endClock=millis();
      Serial.print("Execution took ");
      Serial.print(endClock-startClock);
      Serial.print(" milliseconds\n");
      startClock = millis();
    #endif
  }  
}
 
This is really impressive. It has full sd/Ethernet library support? I had the same problem with my arduino program and had to edit out DNS support to get it to fit (web server/HTML control for ebrewing). I came in 200 bytes under the limit in the flash. How hard would this be to adapt for another sketch?

Edit: added link

Description of my project here: https://www.homebrewtalk.com/f170/brewboard-arduino-webpage-control-e-brewery-445976/
 
This definitely does not have full Ethernet library support. All it is doing at the moment is opening a socket to a server, sending data through that socket, and then closing the socket. I don't think it would be terribly difficult for me to add the ability to receiving data through the socket although networking is very much not my strong suit. There are all sorts of goodies in ethernet like DNS that I removed since they were unnecessary to my project. Still, like you I had planned on this code serving as the springboard to a webserver so I think I'll work towards a send and receive ability. I was thinking about doing the same treatment with my ENC28J60 but honestly going through another 200 page datasheet and learning another ethernet chip sounds a little too daunting this month. Okay the ENC28J60's is only 102 pages but still!
 
The Ethernet and sd libraries are huge. DNS alone saved me 2KB. I think I saw one that had cut it down about in half with full functionality called "tiny Ethernet" or something but it may just be easier to upgrade to the Mega. Either way, impressive optimization on your part. :mug:
 

Latest posts

Back
Top