3

I'm building a final project for a class that involves playing an audio file of a wave shield every time I scan an amiibo with an RFID scanner. I'm a pretty new programmer, so I'm not very familiar with the how to best reduce DRAM usage. As such, I got the following error when I attempted to run my most recent iteration of code:

Global variables use 2801 bytes (136%) of dynamic memory, leaving -753 bytes for local variables. Maximum is 2048 bytes.

Here is my code:

////// WAVE SHIELD HEADERS //////
#include <FatReader.h>
#include <SdReader.h>
#include <avr/pgmspace.h>
#include "WaveUtil.h"
#include "WaveHC.h"
/////////////////////////////////

/////// LCD HEADERS /////////////
#include <Wire.h> 
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27,16,2);  // set the LCD address to 0x27 for a 16 chars and 2 line display

//////////////////////////////////////////

////////// RFID HEADERS /////////////////
#include <SPI.h>
#include <Adafruit_PN532.h>

#define PN532_IRQ   (6)
#define PN532_RESET (3)  // Not connected by default on the NFC Shield

Adafruit_PN532 nfc(PN532_IRQ, PN532_RESET);

/////// various declarations /////////
uint8_t GLOBALUID[7];


//////////////////////////// WAVE SHIELD PORTION OF CODE ///////////////////////////////////


SdReader card;    // This object holds the information for the card
FatVolume vol;    // This holds the information for the partition on the card
FatReader root;   // This holds the information for the filesystem on the card
FatReader f;      // This holds the information for the file we're play

WaveHC wave;      // This is the only wave (audio) object, since we will only play one at a time

#define DEBOUNCE 100  // button debouncer

// this handy function will return the number of bytes currently free in RAM, great for debugging!   
int freeRam(void)
{
  extern int  __bss_end; 
  extern int  *__brkval; 
  int free_memory; 
  if((int)__brkval == 0) {
    free_memory = ((int)&free_memory) - ((int)&__bss_end); 
  }
  else {
    free_memory = ((int)&free_memory) - ((int)__brkval); 
  }
  return free_memory; 
} 

void sdErrorCheck(void)
{
  if (!card.errorCode()) return;
  putstring("\n\rSD I/O error: ");
  Serial.print(card.errorCode(), HEX);
  putstring(", ");
  Serial.println(card.errorData(), HEX);
  while(1);
}

/////// MAIN SETUP /////////

void setup() {
  // set up serial port
  Serial.begin(115200);
  
  // Set the output pins for the DAC control in the wave shield.
  pinMode(2, OUTPUT);
  pinMode(3, OUTPUT);
  pinMode(4, OUTPUT);
  pinMode(5, OUTPUT);

  // Set button outputs
  pinMode(A0, INPUT);
  pinMode(A1, INPUT);
  
  // Set random seed for picking amiibo
  randomSeed(analogRead(A2));

  // initialize lcd
  lcd.init();
  lcd.init();
  lcd.backlight();
  lcd.setCursor(0,0);
  
   if (!card.init()) {
    putstring_nl("Card init. failed!");
    sdErrorCheck();
    while(1);
  }
  
  // enable optimize read - some cards may timeout. Disable if you're having problems
  card.partialBlockRead(true);
 
// look for a FAT partition
  uint8_t part;
  for (part = 0; part < 5; part++) {
    if (vol.init(card, part)) 
      break;
  }
  if (part == 5) {
    putstring_nl("No valid FAT partition!");
    sdErrorCheck();
    while(1);
  }
  
  // tell the user about what was found
  putstring("Using partition ");
  Serial.print(part, DEC);
  putstring(", type is FAT");
  Serial.println(vol.fatType(),DEC);     // FAT16 or FAT32?
  
  // Try to open the root directory
  if (!root.openRoot(vol)) {
    putstring_nl("Can't open root dir!");
    while(1);
  }
}
//////////// END OF WAVE SHIELD ///////////////


////////////// FUNCTION FOR SELECTING AUDIO FILES ////////////
void playSoundClip(){

  char charUID[7];

  for(int i = 0; i < 7; i++)
    charUID[i] = GLOBALUID[i];

  Serial.println();

/*
      {"Mario",                   { 4, 79, 119, 42, 55, 60, 128 },"MARIO"},     //0
      {"Meta Knight",             { 4, 243, 209, 58, 117, 76, 128},"MK"},       //1
      {"Inkling",                 { 4, 84, 47, 218, 87, 73, 128 },"INK"},       //2
      {"Donkey Kong",             { 4, 244, 122, 98, 249, 61, 128 },"DK"},      //3
      {"Lucas",                   { 4, 7, 52, 202, 178, 73, 129 },"LUCAS"},     //4
      {"Duck Hunt",               { 4, 40, 173, 146, 84, 73, 128 },"DUCKH"},    //5
      {"Ocarina of Time Link",    { 4 163 57 114 221 76 128 },"OOTL"},          //6
      {"Breath of The Wild Link", ( 4 133 61 74 120 76 128 },"BOTWL"},          //7
      {"8-bit Link",              { 4 192 150 130 221 76 128 },"8BITL"},        //8
      {"Toon Link",               { 4, 89, 88, 226, 161, 62, 128 },"TOONL"},    //9      
      {"Mr. Game and Watch",      { 4, 134, 32, 154, 85, 73, 128 },"GANDW"},    //10
      {"Falco",                   { 4, 23, 139, 178, 17, 74, 129 },"FALCO"},    //11
      {"King Dedede",             { 4, 108, 188, 242, 232, 73, 128 },"DEDEDE"}, //12
      {"Mewtwo",                  { 4, 241, 126, 202, 87, 73, 128 },"MEWTWO"},  //13
      {"Captain Falcon",          { 4, 11, 204, 178, 124, 72, 129 },"CAPF"},    //14
      {"ROB",                     { 4, 40, 208, 50, 3, 73, 128 },"ROB"},        //15
      {"PAC-MAN",                 { 4, 45, 26, 2, 114, 64, 129 },"PACMAN"},     //16
      {"Cloud",                   { 4, 107, 128, 26, 114, 64, 128 },"CLOUD"},   //17
      {"Ness",                    { 4, 107, 128, 26, 114, 64, 128 },"NESS"},    //18
      {"Ryu",                     { 4, 109, 252, 234, 33, 75, 128 },"RYU"}      //19
*/

  uint8_t amiiboNo = 0;
  
  if(strcmp(charUID,"479119425560128"))
    amiiboNo = 0; //Mario
  else if(strcmp(charUID,"42432095811776128"))
    amiiboNo = 1; //Meta Knight
  else if(strcmp(charUID,"484472188773128"))
    amiiboNo = 2; //Inkling
  else if(strcmp(charUID,"42441229824961128"))
    amiiboNo = 3;// Donkey Kong
  else if(strcmp(charUID,"475220217873129"))
    amiiboNo = 4;// Lucas
  else if(strcmp(charUID,"4401731468473128"))
    amiiboNo = 5;// Duck Hunt
  else if(strcmp(charUID,"41635711422176128"))
    amiiboNo = 6;// Ocarina of Time Link
  else if(strcmp(charUID,"4133617412076128"))
    amiiboNo = 7;// Breath of The Wild Link
  else if(strcmp(charUID,"419215013022176128"))
    amiiboNo = 8;// 8-bit link
  else if(strcmp(charUID,"4898822616162128"))
    amiiboNo = 9;// toon link
  else if(strcmp(charUID,"4134321548573128"))
    amiiboNo = 10;// Mr. Game and Watch
  else if(strcmp(charUID,"4231391781774129"))
    amiiboNo = 11;// Falco
  else if(strcmp(charUID,"410818824223273128"))
    amiiboNo = 12;// king dedede
  else if(strcmp(charUID,"42411262028773128"))
    amiiboNo = 13;// Mewtwo
  else if(strcmp(charUID,"41120417812472129"))
    amiiboNo = 14;// Captain Falcon
  else if(strcmp(charUID,"44020850373128"))
    amiiboNo = 15;// ROB
  else if(strcmp(charUID,"44526211464129"))
    amiiboNo = 16;// Pacman
  else if(strcmp(charUID,"424913721823376128"))
    amiiboNo = 17;// Cloud
  else if(strcmp(charUID,"41071282611464128"))
    amiiboNo = 18;// Ness
  else if(strcmp(charUID,"41092522343375128"))
    amiiboNo = 19;// Ryu

  Serial.println(amiiboNo);
  
  uint8_t clipNo = random(3);
  
  switch(amiiboNo){
    case 0:
      if(clipNo == 0)
        playComplete("MARIO01.WAV");
      else if(clipNo == 1)
        playComplete("MARIO02.WAV");
      else if(clipNo == 2)
        playComplete("MARIO03.WAV");
      break;
    case 1:
      if(clipNo == 0)
        playComplete("MK01.WAV");
      else if(clipNo == 1)
        playComplete("MK02.WAV");
      else if(clipNo == 2)
        playComplete("MK03.WAV");
      break;
    case 2:
      if(clipNo == 0)
        playComplete("INK01.WAV");
      else if(clipNo == 1)
        playComplete("INK02.WAV");
      else if(clipNo == 2)
        playComplete("INK03.WAV");
       break;
    case 3:
      if(clipNo == 0)
        playComplete("DK01.WAV");
      else if(clipNo == 1)
        playComplete("DK02.WAV");
      else if(clipNo == 2)
        playComplete("DK03.WAV");
       break;
    case 4:
      if(clipNo == 0)
        playComplete("LUCAS01.WAV");
      else if(clipNo == 1)
        playComplete("LUCAS02.WAV");
      else if(clipNo == 2)
        playComplete("LUCAS03.WAV");
       break;
    case 5:
      if(clipNo == 0)
        playComplete("DUCKH01.WAV");
      else if(clipNo == 1)
        playComplete("DUCKH02.WAV");
      else if(clipNo == 2)
        playComplete("DUCKH03.WAV");
       break;
    case 6:
      if(clipNo == 0)
        playComplete("OOT01.WAV");
      else if(clipNo == 1)
        playComplete("OOT02.WAV");
      else if(clipNo == 2)
        playComplete("OOT03.WAV");
       break;
    case 7:
      if(clipNo == 0)
        playComplete("BOTWL01.WAV");
      else if(clipNo == 1)
        playComplete("BOTWL02.WAV");
      else if(clipNo == 2)
        playComplete("BOTWL03.WAV");
       break;
    case 8:
      if(clipNo == 0)
        playComplete("8BITL01.WAV");
      else if(clipNo == 1)
        playComplete("8BITL02.WAV");
      else if(clipNo == 2)
        playComplete("8BITL03.WAV");
       break;
    case 9:
      if(clipNo == 0)
        playComplete("TOONL01.WAV");
      else if(clipNo == 1)
        playComplete("TOONL02.WAV");
      else if(clipNo == 2)
        playComplete("TOONL03.WAV");
       break;
    case 10:
      if(clipNo == 0)
        playComplete("GANDW01.WAV");
      else if(clipNo == 1)
        playComplete("GANDW02.WAV");
      else if(clipNo == 2)
        playComplete("GANDW03.WAV");
       break;
    case 11:
      if(clipNo == 0)
        playComplete("FALCO01.WAV");
      else if(clipNo == 1)
        playComplete("FALCO02.WAV");
      else if(clipNo == 2)
        playComplete("FALCO03.WAV");
       break;
    case 12:
      if(clipNo == 0)
        playComplete("DEDEDE01.WAV");
      else if(clipNo == 1)
        playComplete("DEDEDE02.WAV");
      else if(clipNo == 2)
        playComplete("DEDEDE03.WAV");
       break;
    case 13:
      if(clipNo == 0)
        playComplete("MEWTWO01.WAV");
      else if(clipNo == 1)
        playComplete("MEWTWO02.WAV");
      else if(clipNo == 2)
        playComplete("MEWTWO03.WAV");
       break;
    case 14:
      if(clipNo == 0)
        playComplete("CAPF01.WAV");
      else if(clipNo == 1)
        playComplete("CAPF02.WAV");
      else if(clipNo == 2)
        playComplete("CAPF03.WAV");
       break;
    case 15:
      if(clipNo == 0)
        playComplete("ROB01.WAV");
      else if(clipNo == 1)
        playComplete("ROB02.WAV");
      else if(clipNo == 2)
        playComplete("ROB03.WAV");
       break;
    case 16:
      if(clipNo == 0)
        playComplete("PACMAN01.WAV");
      else if(clipNo == 1)
        playComplete("PACMAN02.WAV");
      else if(clipNo == 2)
        playComplete("PACMAN03.WAV");
       break;
    case 17:
      if(clipNo == 0)
        playComplete("CLOUD01.WAV");
      else if(clipNo == 1)
        playComplete("CLOUD02.WAV");
      else if(clipNo == 2)
        playComplete("CLOUD03.WAV");
       break;
    case 18:
      if(clipNo == 0)
        playComplete("NESS01.WAV");
      else if(clipNo == 1)
        playComplete("NESS02.WAV");
      else if(clipNo == 2)
        playComplete("NESS03.WAV");
       break;
    case 19:
      if(clipNo == 0)
        playComplete("RYU01.WAV");
      else if(clipNo == 1)
        playComplete("RYU02.WAV");
      else if(clipNo == 2)
        playComplete("RYU03.WAV");
       break;
       
    }
}

//////// MAIN LOOP /////////
void loop() {

      lcd.print("Scan an");
      lcd.setCursor(0,1);
      lcd.print("amiibo!");
      delay(3000);
    
    uint8_t * ID{};
        
    waitForRFID();
    
    playSoundClip();
    
  
}


//////////////// MORE WAVE SHIELD ///////////////////////

// Plays a full file from beginning to end with no pause.
void playComplete(char *name) {
  // call our helper to find and play this name
  playfile(name);
  while (wave.isplaying) {
  // do nothing while its playing
  }
  // now its done playing
}

void playfile(char *name) {
  // see if the wave object is currently doing something
  if (wave.isplaying) {// already playing something, so stop it!
    wave.stop(); // stop it
  }
  // look in the root directory and open the file
  if (!f.open(root, name)) {
    putstring("Couldn't open file "); Serial.println(name); return;
  }
  // OK read the file and turn it into a wave object
  if (!wave.create(f)) {
    putstring_nl("Not a valid WAV"); return;
  }
  
  // ok time to play! start playback
  wave.play();
}

/////////////// END OF WAVE SHIELD //////////////////////

//////////////// RFID FUNCTION /////////////////////////
uint8_t* waitForRFID(){
    nfc.SAMConfig();
  
  Serial.println("Waiting for an amiibo ...");

  uint8_t success;
  uint8_t uid[] = { 0, 0, 0, 0, 0, 0, 0 };
  uint8_t uidLength;
  uint8_t testuid[7];
  while(true){
    if (nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength)){

      Serial.print("Hex UID: ");
      nfc.PrintHex(uid, uidLength);
      Serial.print("Octal UID: { ");
      for(int i = 0; i < uidLength; i++){
        Serial.print(uid[i]);
        GLOBALUID[i] = uid[i];
        if (i != uidLength - 1)
          Serial.print(", ");
      }
      Serial.println(" }");

      Serial.print("GLOBALUID: ");
      for(int i = 0; i < 7; i++)
        Serial.print(GLOBALUID[i]);
        
      Serial.println();
      Serial.println();
    }
    return testuid;
  }
  
}

//////////////// END OF RFID FUNCTION /////////////////

My first guess for what may be taking up the most memory is the switch statement. When I uncomment that, the memory usage jumps from 90% to over 130%. I've tried other ways to get this code to work, like concatenating text, but this is the only way that seemed to work reliably, until I ran out of memory.

I'm also using a lot of libraries to help use the shields and LCD I have attached to the Uno. I couldn't get rid of those libraries without compromising the project.

Other than that, I don't know what to do. If anyone has any suggestions for condensing the code down, I'd really appreciate any advice you have to offer.

Thanks.

4
  • 1
    Honestly I don't see much potential for optimziation without touching the libraries. Commenting the switch case gives you that much memory, because the compiler then optimizes out all the wav playing stuff, that is only used in the switch statement. Maybe you should switch to a board with more RAM
    – chrisl
    May 8, 2021 at 20:32
  • I think you can put all the string literals in program space with the string helper, but I don't think, that this will be enough, since you want to get to maybe 75% (the compiler doesn't count all local variables I believe)
    – chrisl
    May 8, 2021 at 20:34
  • The memory usage here clearly comes from the libraries you use. The combination of SD card reader library + LCD + NFC reader seems just too big for an Arduino Uno. I'm unable to build your code, as I can't find the libraries you're using. Are these standard libraries installed trough the library manager?
    – PMF
    May 9, 2021 at 6:40
  • 1
    nitpick: Arduino Uno has an ATmega328, which has SRAM, not DRAM. The phrase "dynamic memory" seems to mean just dynamic in a more generic sense, i.e. volatile memory used at run-time, as opposed to the non-volatile program memory that's mostly static wrt. run-time use. On the other hand, DRAM is a particular way of implementing (volatile) RAM.
    – ilkkachu
    May 10, 2021 at 11:25

3 Answers 3

11

You have a huge switch...case here to decide the name of the file that should be played. This costs a lot of code space, but also data space, because every single literal string in this portion of code gets copied to RAM during initialization.

There is no need to spend all this cost, as the file names have been assigned in a very regular fashion, so you can build the file name algorithmically. As a general rule, whenever you find yourself programming by copying, pasting, then applying some predictable modifications to the copies, you are almost certainly doing something wrong. Making repeated copies of the same thing, with predictable changes is what computers are best for. So you have to ask yourself how you can instruct the computer (in this case, the Arduino) to do that for you. Do not do the computer's job yourself!

In this case, the file name is built by gluing three pieces together:

  • a prefix that depends on amiiboNo
  • a two-digit number between "01" and "03", depending on clipNo
  • the constant suffix ".WAV"

The prefix cannot be computed algorithmically, so you still have to write down all the possible prefixes. You could use a big switch...case for this, but it is way more convenient to pick the right prefix from an array indexed by amiiboNo:

const char *file_prefixes[] = {
    "MARIO", "MK",    "INK",    "DK",    "LUCAS", "DUCKH",  "OOT",
    "BOTWL", "8BITL", "TOONL",  "GANDW", "FALCO", "DEDEDE", "MEWTWO",
    "CAPF",  "ROB",   "PACMAN", "CLOUD", "NESS",  "RYU"
};

Then, you declare an array of char for holding the file name, and you build that file name by putting together the three pieces listed above. Your whole switch...case can then be replaced by this:

char filename[13];           // longest file name: 12 characters + NUL
char file_number[3] = "01";  // 2 digits + NUL
file_number[1] += clipNo;    // file_number: "01", "02" or "O3"
strcpy(filename, file_prefixes[amiiboNo]);
strcat(filename, file_number);
strcat(filename, ".WAV");
playComplete(filename);

This will already save you a lot of RAM, but probably not enough. I did not see any obvious inefficient use of variables in your program (but I did not look in too much detail), so we will continue to aim at literal strings.

The problem with literal strings is the fact that they get copied from flash to RAM during initialization, and whereas you probably have enough flash, the RAM is in short supply. This is a problem specific to Harvard architectures, as they have separate address spaces for code and data. We can use some AVR-specific tricks to instruct the compiler not to perform this copying. This makes it harder to access the data because C++ does not natively know about address spaces. Fortunately, there are some macros and helper functions that can make it easy in specific cases.

The best know such helper in Arduino world is the F() macro. Use it every time you print() or println() a literal string, as in:

Serial.println(F("Waiting for an amiibo ..."));

Next, we have to get rid of the very long list of numeric UIDs. For this, we use the PSTR() macro and strcmp_P() function provided by the avr-libc. Replace every line that looks like

if (strcmp(charUID, "479119425560128"))

by something like

if (strcmp_P(charUID, PSTR("479119425560128")) == 0)

Note that the value returned by strcmp_P() has to be compared to 0, as this is what that function, just like strcmp(), returns in case of equality. Thanks to timemage for noticing this issue.

Last, we could also keep all the file prefixes in flash, but this would be trickier. For now, I suggest you apply the tricks described here and see whether they are enough to make your program fit.

Edit: more on the somewhat obscure statement: file_number[1] += clipNo;

In C++, a char is an 8-bit number. Depending on the platform, it can be either signed (range: −128…+127) or unsigned (0…255). The name comes from the fact that it is most often used to store the numeric value of an ASCII character. For example, the numeric value 33 means '!', 48 means '0', and 65 means 'A'.

The definition

char file_number[3] = "01";

creates a 3-cell array initialized with the following content:

             ┌─────┬─────┬─────┐
file_number: │  48 │  49 │   0 │
             └─────┴─────┴─────┘

Using the language notation for characters, the contents could also be written as follows:

             ┌─────┬─────┬─────┐
file_number: │ '0' │ '1' │ '\0'│
             └─────┴─────┴─────┘

The last cell with the ASCII NUL is there to inform library functions such as strcat() that this is where the string of characters ends.

The statement

file_number[1] += clipNo;

accesses the second element of the array (array indexing starts at 0) and increments its numeric value by the amount of clipNo. For example, if clipNo is 1, then the value in the middle cell will be changed from 49 (i.e. '1') to 50 (i.e. '2'). This relies on the fact that ASCII digits have consecutive numeric values, which would also be expected from any sane character encoding.

3
  • 1
    I know this originated in the O.P.'s code, and you I know that you know this, but strcmp(_P) returns 0 for "equal".
    – timemage
    May 9, 2021 at 11:48
  • @timemage: Oops! You are right. I know but I didn't pay attention. I added that to the answer, just to make it more prominent. May 9, 2021 at 13:12
  • As soon as I get the opportunity, I’ll implement these fixes and get back with you on the progress. Thank you for the help! May 9, 2021 at 15:18
4

As a minor addendum to Edgar Bonet's excellent answer, you could simplify your code further and save even more memory by renaming your files.

In particular, instead of storing a list of character names indexed by Amiibo number and using those as the filename prefix, why not use the Amiibo numbers directly? That is, instead of your files being named

MARIO01.WAV, MARIO02.WAV, MARIO03.WAV, MK01.WAV, MK02.WAV, MK02.WAV, etc.

you could just name them e.g.

AMI01_01.WAV, AMI01_02.WAV, AMI01_03.WAV, AMI02_01.WAV, AMI02_02.WAV, AMI02_03.WAV, etc.

and construct the name in your code e.g. like this:

char filename[13];
int len = snprintf(filename, sizeof(filename), "AMI%02d_%02d.WAV", amiiboNo + 1, clipNo + 1);
if (len < 0 || len >= sizeof(filename)) {
  // TODO: handle error or insufficient buffer size here
}

The main advantage here is that you'll save memory by not having to store the list of character names. The tradeoff, of course, is that your filenames will now be a lot less descriptive.

You could probably optimize this a bit more by manipulating the bytes of the filename directly like Edgar does. But personally I'd suggest starting with the generic snprintf based solution and only switching to direct byte manipulation if it's still too much. At least I'd assume that the snprintf implementation in the standard library is probably going to be fairly well optimized for a low footprint. Of course I could be wrong.

3
  • 1
    snprintf() is a good suggestion. It will likely cost more flash than strcpy() and strcat() combined because, even if you only use %02d, the function carries support for a wide range of conversion specifiers. I do think it is a good choice, if you have enough flash (likely the case here), because it makes the code simpler. It's interface, however, may not be very novice-friendly. OTOH, my file_number[1] += clipNo; statement is probably not novice-friendly either. :-/ May 9, 2021 at 20:57
  • @EdgarBonet, as a novice, I can attest, I was a bit thrown off by the file_number[1] += clipNo; statement, haha. If you have the time, would you mind explaining what you did there? May 9, 2021 at 21:47
  • Update: It works! After changing the string literals and file names via yours' and @EdgarBonet 's suggestions, my program sits at 84% memory. I'll probably tamper around with it a bit more to eek out some extra features. Thank you both! May 9, 2021 at 23:52
1

Just a SWAG try switching from a UNO to a MEGA the code should work once the decelerations are fixed. What the compiler is not telling you is how much memory is actually used. You have the Heap and Stack etc.

3
  • I’ve considered getting a Mega; it seems worthwhile. Unfortunately this is a project that’s due in a few days. I may grab it just to satiate my own curiosity though. Thanks for the suggestion. May 9, 2021 at 15:15
  • 1
    I generally start with a larger processor such as a Mega and when finished I can put it in a smaller unit. This gives me room for the debugging print statements etc.
    – Gil
    May 11, 2021 at 2:02
  • So, I borrowed a Mega from my professor and tried it out yesterday. As it turns out, the wave shield I'm using to read audio files and play to a speaker explicitly lists the Mega as a version of Arduino that is NOT supported, for some reason. It really sucks; I wanted to expand the scope of this project significantly, but what can you do? May 12, 2021 at 4:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.