Arduino Door Controller – Being Frugal with RAM

So I had the opportunity this weekend to spend some time finally starting development work with my Arduino. A simple start I thought – import some libraries and start cranking out the code.

As I’ve mentioned previously I’m planning on working on a smart door lock for my house with RFID and potentially other sensors in the future as well (I’m considering Bluetooth beacons at the moment). The basic functionality required will be NFC, Ethernet (with PoE), configuration on-device via SD card and a telnet interface for management of the device’s configuration.

For those of you who don’t know you’ve got 32KB of available space on the Arduino of which 2KB is reserved for the standard boot loader. I want to also implement a boot loader that allows firmware to be updated whenever the device power cycles which may take up an additional 2KB leaving me with 28KB for my application.

Having created a basic hello world application I imported all the libraries this project would require and came out with 25KB of code straight away. Pretty quickly I realised there’s going to need to be some restraint with my code here. I had planned to create a telnet connection that allowed multiple inbound clients each handled with protothreads and their own C++ classes to control them but decided to keep it simple for now.

I progressed pretty well with the network interface side of things using the Ethernet module and using the third party IniFile and standard SD card modules to load configuration values up. As I was extending the functionality of my configuration menu I kept encountering weird issues. For no apparent reason the Arduino would reset, gibberish would come out of the serial port or it would just stop working.

I kept going through my code line by line commenting stuff out (debugging like it’s 1989) to find really strange lines would cause issues in ways that made no sense. Some were just debug lines and some were buffer allocations for string parsing.

I’m ashamed to say, coming from a non-embedded development background, that it took me two hours before I finally considered how much memory the Arduino had available to it. The answer is 2KB. It’s been quite some time since I’ve been constrained by 2KB.

I immediately realised that not only was the buffer I was using to parse input causing the memory to exceed available limits almost immediately (without thinking I just created a 1KB buffer for parsing input) but I also had a lot of debugging information in the form of Serial.println() statements so that I could see what was going on via my serial debugger. Each string I’d created was using valuable memory. The buffer was easy to fix and reducing that slightly stabilised my program. The strings, when all commented out, led to my sketch running fine – but I wanted the debug information.

Thankfully there are a few options to deal with when you need a lot of strings. You have the option to store these strings in your flash memory (program space) rather than in SRAM alone and only load them when required.

You’ll find various solutions if you Google on how to do this but by far the easiest for my simple string scenario was to use the F pre-processor. The simple statement

Serial.println(“Hello World!”);

Becomes

Serial.println(F(“Hello World!”));

This instructs the compiler to ensure that the string isn’t placed in RAM but is read from flash whenever the application starts.

My application in its current state is working well and the code is embedded below for your reference if you’re interested. I haven’t yet got around to setting this up on Bitbucket.

At the start of my first day of embedded programming I genuinely thought the 32KB of space to store the application code and boot loader would be my problem but quickly realised 32KB is an enormous amount of space compared to 2KB of RAM. The good news is if anything gets super complicated there are ways to address more RAM if you fancy adding some SIMMS to your Arduino, or just pair your Arduino with something designed to handle the heavy lifting side like a Raspberry Pi. Just for fun and a change of scene though, I’m going to try and see how much I can cram into 2KB.

Not going to get a chance for any updates this weekend as we’re away with family but hoping to finish the network server and add in config saving and loading early next week before actually getting the NFC working. Soldering components have now turned up so hoping to get the whole project done just in time for Christmas.

Core behind read more…

#include 
#include 
#include 
#include 
#include <EthernetClient.h>
#include 
#include 
//#include 
#include 

// Defines the pins we are using
#define SD_SELECT 4
#define ETHERNET_SELECT 10
#define NFC_IRQ (2)
#define NFC_RESET (3)

// Defines our NFC instance
//Adafruit_NFCShield_I2C nfc(NFC_IRQ, NFC_RESET);

// The name of our config file
const char *configFilename = "/config.ini";

// Network settings
byte networkMac[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 };
IPAddress networkIpAddress(192, 168, 200, 200);
IPAddress networkGateway(192, 168, 200, 1);
IPAddress networkSubnet(255, 255, 255, 0);

// The network server that will accept inbound console requests
EthernetServer consoleServer(23);

// Defines whether or not a client is connected to our console
boolean isConsoleConnected = false;
boolean consoleAwaitingMenuSelection = false;
boolean consoleAwaitingSubnet = false;
boolean consoleAwaitingIpAddress = false;
boolean consoleAwaitingGateway = false;
boolean consoleAwaitingMac = false;
EthernetClient connectedClient = NULL;
const int BUFFER_SIZE = 50;
char connectedClientReadBuffer[BUFFER_SIZE];
int connectedClientBufferLocation = 0;

void setup()
{
 // Configure all of the SPI select pins as outputs and make SPI
 // devices inactive, otherwise the earlier init routines may fail
 // for devices which have not yet been configured.
 pinMode(SD_SELECT, OUTPUT);
 digitalWrite(SD_SELECT, HIGH); // disable SD card
 
 pinMode(ETHERNET_SELECT, OUTPUT);
 digitalWrite(ETHERNET_SELECT, HIGH); // disable Ethernet
 
 // Set serial for 9600
 Serial.begin(9600);
 Serial.println(F("Startup serial completed"));
 
 /*
 // Start the SD card
 SPI.begin();
 if (!SD.begin(SD_SELECT))
 {
 Serial.println(F("Failed to start SD, halting"));
 while (1) { delay(500); }
 }
 Serial.println(F("SD card initialised"));
 
 // Load configuration settings
 LoadConfigSettings();
 Serial.println(F("Configuration initialised"));
 */
 
 // Start the NFC
// nfc.begin();
// nfc.setPassiveActivationRetries(1);
// Serial.println(F("Started RFID reader"));
 
 // Setup networking
 Ethernet.begin(networkMac, networkIpAddress, networkGateway, networkSubnet);
 Serial.print(F("Network configured with "));
 Serial.println(Ethernet.localIP());
 consoleServer.begin();
 Serial.println(F("Console server listening on port 23"));
 
 // We're done
 Serial.println(F("Startup completed"));
}

void loop()
{
// ReadRfid();
 HandleTelnetSession();
}

/*
void ReadRfid()
{
 uint8_t success;
 uint8_t uid[] = { 0, 0, 0, 0, 0, 0, 0 }; // Buffer to store the returned UID
 uint8_t uidLength; // Length of the UID (4 or 7 bytes depending on ISO14443A card type)
 
 // Wait for an ISO14443A type cards (Mifare, etc.). When one is found
 // 'uid' will be populated with the UID, and uidLength will indicate
 // if the uid is 4 bytes (Mifare Classic) or 7 bytes (Mifare Ultralight)
 success = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength);
 
 if (!success)
 // Nothing to read here
 return;

 // Display some basic information about the card
 Serial.print(F("UID Length: "));
 Serial.print(uidLength, DEC);
 Serial.println(F(" bytes"));
 Serial.print(F("UID Value: "));
 nfc.PrintHex(uid, uidLength);
 Serial.println(F(""));
}
*/

void HandleTelnetSession()
{
 if (!isConsoleConnected) {
 EthernetClient client = consoleServer.available();
 if (client) {
 // This is a new client, send the initial menu to them and setup state
 client.flush(); // Ignore any potential telnet control characters that have been dumped
 connectedClient = client;
 isConsoleConnected = true;
 consoleAwaitingSubnet = false;
 consoleAwaitingIpAddress = false;
 consoleAwaitingGateway = false;
 consoleAwaitingMac = false;
 Serial.println(F("New console client connected"));
 SendMenu(client);
 }
 }
 else
 {
 // Check if client is still connected, if not free everything up
 if (!connectedClient.connected())
 {
 connectedClient = NULL;
 isConsoleConnected = false;
 Serial.println(F("Console client has disconnected"));
 return;
 }
 

 // If there's no data to read then just wait for a future callback
 int bytesToRead = connectedClient.available();
 if (bytesToRead < 1)
 {
 return;
 }
 
 // There is data to read, so read it until a line break
Serial.print(F("Reading input bytes: "));
Serial.println(bytesToRead);

 boolean newlineEncountered = false;
 for (int i = 0; i < bytesToRead; i++)
 {
 char b = connectedClient.read();
 if (newlineEncountered)
 {
 // Ignore further input
 continue;
 }
 if (b == '\r' || b == '\n')
 {
 // Mark that we've received it, but do not store in our buffer - instead continue ignoring further input
 newlineEncountered = true;
 continue;
 }
 connectedClientReadBuffer[connectedClientBufferLocation] = b;
 connectedClientBufferLocation++;
 
 // If we've exceeded our maximum input count log a buffer overflow and wrap buffer around
 if (connectedClientBufferLocation == BUFFER_SIZE - 1)
 {
 Serial.println(F("Buffer overflow from connected client, wrapping around"));
 connectedClientBufferLocation = 0;
 }
 }
 if (newlineEncountered)
 {
 // Terminate with a null
 connectedClientReadBuffer[connectedClientBufferLocation] = '';
 }
 else
 {
 // We're missing newline, wait for another read
 return;
 }
 
 // Handle menu selection
 if (consoleAwaitingMenuSelection)
 {
 connectedClient.println(F(""));
 consoleAwaitingMenuSelection = false;
 if (!strcmp(connectedClientReadBuffer, "1"))
 {
 connectedClient.print(F("Current IP: "));
 connectedClient.println(Ethernet.localIP());
 connectedClient.print(F("New IP: "));
 Serial.println(F("Console client selecting IP address"));
 consoleAwaitingIpAddress = true;
 }
 else
 {
 connectedClient.println(F("Invalid selection"));
 connectedClient.println(F(""));
 SendMenu(connectedClient);
 Serial.println(F("Console client made invalid selection"));
 }
 }
 
 // Handle entry of IP address
 else if (consoleAwaitingIpAddress)
 {
 if (connectedClientBufferLocation > 15 || connectedClientBufferLocation < 7)
 {
 connectedClient.println(F("Invalid IP address"));
 Serial.println(F("Console client entered invalid IP address"));
 }
 else
 {
 Serial.println(F("Szize is OK"));
 boolean parseError = false;
 int partCount = 0;
 Serial.println(F("Attempting to parse IP buffer"));
 char * buff = strtok(connectedClientReadBuffer, ".");
 int ipParts[4];
 while (buff != NULL)
 {
 Serial.println(buff);
 String s = String(buff);
 int i = s.toInt();
 if ((i == 0 && buff[0] != '0') || (!String(i).equals(s)))
 {
 parseError = true;
 break;
 }
 ipParts[partCount] = i;
 partCount++;
 buff = strtok(NULL, ".");
 if (partCount == 4 && buff != NULL)
 {
 parseError = true;
 break;
 }
 }
 if (parseError)
 {
 connectedClient.println(F("Invalid IP address"));
 Serial.println(F("Console client entered invalid IP address"));
 }
 else
 {
 Serial.print(F("User has entered new IP"));
 Serial.print(ipParts[0]);
 Serial.print(F("."));
 Serial.print(ipParts[1]);
 Serial.print(F("."));
 Serial.print(ipParts[2]);
 Serial.print(F("."));
 Serial.print(ipParts[3]);
 }
 }
 
 // Revert to menu out of IP section
 connectedClient.println(F(""));
 consoleAwaitingIpAddress = false;
 SendMenu(connectedClient);
 }
 
 // Empty out the buffer
 connectedClientBufferLocation = 0;
 }
}

/*
char connectedClientReadBuffer[1024];
char connectedClientBufferLocation = 0;
EthernetClient connectedClient
boolean isConsoleConnected = false;
boolean consoleAwaitingMenuSelection = false;
boolean consoleAwaitingSubnet = false;
boolean consoleAwaitingIpAddress = false;
boolean consoleAwaitingGateway = false;
boolean consoleAwaitingMac = false;
*/

void SendMenu(EthernetClient client)
{
 client.println(F("1) IP Address"));
 client.println(F("2) Subnet"));
 client.println(F("3) Gateway"));
 client.println(F("4) MAC Address"));
 client.println(F("9) Reboot"));
 client.println(F(""));
 client.print(F("Option: "));
 client.flush();
 consoleAwaitingMenuSelection = true;
}

void LoadConfigSettings()
{
 IniFile config (configFilename);
 if (!config.open())
 {
 Serial.println(F("Unable to open config file, creating default"));
 SaveConfigSettings();
 return;
 }
 
 // TODO: Read real values
}

void SaveConfigSettings()
{
 // TODO: Save the INI file
}
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s