Tangible Computing
10. Memory




10.1 Where is stuff stored?

We now to look at how memory is organized on the Arduino. The smallest chunk of memory is typically a byte (1 byte = 8 bits), and measured in units of

Unit Number of Bytes
kB kilobyte 210 = 1024
MB megabyte 220 = 1024 * 1024
GB giga byte 230 = 1024 * 1024 * 1024
TB terabyte 240 = 1024 * 1024 * 1024 * 1024


The Arduino has 3 kinds of memory. See page 21 of the Mega processor manual for more detail. They details vary between processor types. The numbers below are for the ATmega 2560, our processor.

Program Memory SRAM Data Memory SRAM External Data Memory EEPROM Data Memory There are consequences of these memory constraints: We need to be careful about using data memory, but we have enough program memory to be able to trade code to save data, but possibly at the expense of speed.

For the rest of this discussion, we will focus on the 8K, the data memory, that our program can use.

10.2 How does a program use memory?

Every byte of memory has an address that gives the location of the byte in memory. Addresses typically start at 0 (the lowest address) and increase to the maximum amount of memory minus 1 (the highest address). When we get (load) data from memory, and put (store) data into memory, we need to know the address of where the memory is.

An address must be large enough (in bytes) to hold all the possible locations of the installed memory on a machine. For the arduino, this is 64 kB, which is 16 bits (of unsigned).

The first address in data memory is 0x0000, and is the start of the 512 bytes of registers. So the addresses for it ranges from 0x0000 to 0x01FF

The 8 kB of regular memory starts immediately after, so the address of its first byte is 0x0200, and the address of its last byte is 0x21FF (since 8 kB is 0x2000).

Memory is organized into blocks that are used for different purposes.

Address (hex) Function
0 - 1F 32 Registers
20 - 5F 64 I/O Regusters
60 - 1FF 416 External I/O Registers
200 - 21FF Internal SRAM (8 kB bytes)
2200 - FFFF External SRAM (55.5 kB)




Let's see where data is placed in the Arduino's memory by running the program

code/Memory/Mem01/Mem01.ino

    // put a single byte into our data memory
    // if you remove the initialization (= 1) the
    // location changes from 0x200 to 0x212
    char var_1 = 1;  
     
    void setup() {
      Serial.begin(9600);
      
      // print out its address, which is 16 bits unsigned 
      Serial.println( (uint16_t) &var_1, HEX);
    }
     
    void loop() {
    }


Note the unary & operator (a mnemonic for the address) in Serial.println. It is used to obtain the address of the variable it is being applied to.

We get the hex output 0x200. So it looks like global variables (those seen by all parts of the program) start at the low memory addresses. Note, if we do not initialize var_1, then it starts at 0x212. Something else must be at 0x200. Variables are rearranged so that those requiring initialization are before those that do not.

Test this with code/Memory/Mem02/Mem02.ino

    // put 2 char variables and 2 int variables into our data memory
    char var_1 = 1; 
    uint8_t var_2 = 2;
    int var_3 = 3;
    int16_t var_4 = 4;
     
    void setup() {
      Serial.begin(9600);
      Serial.println("Mem02");
     
      // print out its address, which is 16 bits unsigned 
      Serial.println( (uint16_t) &var_1, HEX); 
      Serial.println( (uint16_t) &var_2, HEX);
      Serial.println( (uint16_t) &var_3, HEX);
      Serial.println( (uint16_t) &var_4, HEX);
    }
     
    void loop() {
    }


Note how we get the addresses as follows
var_1 0x206
var_2 0x207
var_3 0x208
var_4 0x20A
Since var_3 is an int, it takes 2 bytes. So var_4 starts 2 bytes later in memory than var_3. We low to high memory allocation with the global variables in this program.

code/Memory/Mem03/Mem03.ino

    // show how the sizeof operator works
    char var_1 = 1; 
    int16_t var_2 = 2;
    int32_t var_3 = 3;
    void* var_4;  // this is an address
     
    void setup() {
      Serial.begin(9600);  
      Serial.println("Mem03");
      
      // print out its address, which is 16 bits unsigned 
      Serial.print( (uint16_t) &var_1, HEX); 
      Serial.print( " " );
      Serial.println( sizeof(var_1), HEX);
      
      Serial.print( (uint16_t) &var_2, HEX);
      Serial.print( " " );
      Serial.println( sizeof(var_2), HEX);
     
      Serial.print( (uint16_t) &var_3, HEX);
      Serial.print( " " );
      Serial.println( sizeof(var_3), HEX);
     
      Serial.print( (uint16_t) &var_4, HEX);
      Serial.print( " " );
      Serial.println( sizeof(var_4), HEX);
    }
     
    void loop() {
    }


If you run this program you get different addresses again. So we cannot predict our memory use very easily.

We can generate a memory map using avr-nm, part of which is:
000021ff W __stack
00800200 D __data_start
00800208 D var_1
00800209 D var_2
0080020b D var_3
0080020f V _ZTV14HardwareSerial
00800222 B __bss_start
00800222 B var_4
00800222 D __data_end
00800222 D _edata
00800224 B rx_buffer
008002a8 B rx_buffer1
0080032c B rx_buffer2
008003b0 B rx_buffer3
00800434 B Serial
00800447 B Serial1
0080045a B Serial2
0080046d B Serial3
00800480 B timer0_overflow_count
00800484 B timer0_millis
00800488 b timer0_fract
00800489 B __bss_end
00800489 N __heap_start
00800489 N _end


Ignore the leading 008 in the address, it just marks data store.

Remember, we can do arithmetic on byte addresses. For example, we use 0x489 - 0x223 = 0x266 (2 * 256 + 6 * 16 + 6 = 614 bytes) for serial port data. And 18 bytes for the variable _ZTV14HardwareSerial.

And where do the strings sit? Seems to be in the data area. This is not very good since they are using up precious ram! It would be good if we could place formatting strings, which are constant, into the program memory area of the processor, which is much larger. This is in fact possible, you need to look in the avr-libc manual.

10.3 General Notation and Properties of Pointers



10.4 Big Endian and Little Endian

When the information we want to store in memory required more than one byte, we have to understand how it is placed into memory. The first place we normally encounter this is with integers. A 16-bit integer X requires 2 bytes. Suppose that the address of X is 6, that is, the bytes of X are in locations 6 and 7. But are the low 8-bits of X in location 6, and the high 8-bits of X in location 7? Or is it the other way around? Which particular order depends on why kind of endianness the machine uses.

In the first case, where the least significant bytes have the smaller addresses, the machine is called little-endian. In the other case where the most significant bytes have the smaller addresses, the machine is called big-endian.

Here is a simple program that prints out the two bytes that compose a 16-bit unsigned integer, and then deduces what kind of endian machine the Arduino is.

code/Memory/Endian/Endian.ino

    // how are multibyte integers placed in memory?
     
    // some easily identifiable byte patterns
    uint16_t X = 0x1122;
     
    void setup() {
        uint8_t *p;  // a pointer to a byte
     
        Serial.begin(9600);
        Serial.println("Endian Test");
     
        // print the parts of X 
        Serial.print("X is 0x");  Serial.println(X, HEX);
     
        p = (uint8_t *) &X;
     
        Serial.print("p is 0x"); Serial.println( (uint16_t) p, HEX);
        Serial.print("*p is 0x"); Serial.println( *p, HEX); 
        Serial.print("*(p+1) is 0x"); Serial.println( *(p+1), HEX);
     
        if ( *p == 0x11 ) {
            Serial.println("This is a big-endian machine.");
            }
        else {
            Serial.println("This is a little-endian machine.");
            }
       }
     
    void loop() {
    }


When we run this program we get this output:
Endian Test
X is 0x1122
p is 0x271
*p is 0x22
*(p+1) is 0x11
This is a little-endian machine.


Endianness is important when we ship bytes from one machine to another. If they have different endianness, then we need to reorder the bytes that represent any integer that requires more than 1 byte to store.

10.5 The Stack and Heap

And what are these stack and heap things?
000021ff W __stack
00800489 N __heap_start
The stack and heap are regions of memory from which we can allocate and free temporary variables.

The heap starts at the lowest unallocated byte of memory, and "grows" upward (increasing addresses). The stack starts at the highest unallocated byte of memory and grows downward (decreasing addresses). The current amount of free memory is the difference between the top of the stack and the end of the heap.

The stack is used to store data which is used on a last-in first-out basis. It grows and shrinks like a stack of plates, or stack of cards.

The heap is used to store data that has a more unorganized usage pattern, and when data is no longer used is remains in the heap until it is garbage collected.



The stack starts at the top of memory, and works downward. It is where temporary variables are allocated, and then released after use. What do we mean by temporary?

There are two ways that you can obtain storage on the stack,

10.6 Block Variables

Whenever you have a left-curly brace { a new scope of declaration is created. You can introduce new variables, that come into being when you enter the block by being allocated on the stack, and then are removed from the stack when you leave the block at the } curley brace.

Note that { and } always nest in a well defined way, by properly matching the opening { and closing }. For example you can have this:
{
     uint16_t x;
     {
         uint32_t y;
     }
}
but not this:
{
     uint16_t x;
     {
         uint32_t y;
}
     }
This means that any inner block that allocates a variable is always left before any enclosing block, so that the more recently allocated variables are closer to the top of the stack.

Here is a simple example where we need to extract out all the odd numbers in an array to pass on to some other function that does something with them, and returns a result. code/Memory/StackAlloc/stack.cpp

    #include <Arduino.h>
    #include <mem_syms.h>
     
    // return the sum of the elements of an array
    int16_t sum(uint16_t len, int16_t A[]) {
        int16_t result = 0;
        for (uint16_t i=0; i < len; i++) {
            result += A[i];
            }
        return result;
        }
     
    void do_stuff(uint16_t len, int16_t odd_even_data[]) {
        Serial.print("Stack before extracting odd: ");
        Serial.println(STACK_SIZE);
        {
            // create a temporary working array to collect odd elements of data
            int16_t odd_data[len];
            uint16_t odd_len = 0;
            int16_t odd_sum;
     
            for (uint16_t i=0; i < len; i++) {
                if ( odd_even_data[i] & 1 == 1 ) {
                    odd_data[odd_len] = odd_even_data[i];
                    odd_len++;
                    }
                }
     
            Serial.print("Stack after extracting odd: ");
            Serial.println(STACK_SIZE);
     
            odd_sum = sum(odd_len, odd_data);
            Serial.print("Odd sum is ");  Serial.println(odd_sum); 
            }
     
        Serial.print("Stack after leaving block: ");
        Serial.println(STACK_SIZE);
        }
     
    void setup() {
     
        uint16_t i;
        const uint16_t n_elements = 64;
        int16_t data[n_elements];
     
        Serial.begin(9600);
     
        for (i=0; i < n_elements; i++) {
            data[i] = i;
            }
     
        do_stuff(n_elements, data);
        }
     
    void loop() {
        }
        


When this program is run you get this output showing how the stack size increases from 157 bytes to 285 bytes (an increase of 128 bytes) which is just enough to store data (64 * 2 bytes). Note that the storage for the small items, odd_len and odd_sum is probably in registers (for speed) and not on the stack. The compiler optimizations make it difficult to predict exactly where certain variables will be located.
Stack before extracting odd: 157
Stack after extracting odd: 285
Odd sum is 1024
Stack after leaving block: 157
We use this feature to allocate a temporary array S in one of our versions of merge sort in the section on sorting §12.

10.7 Procedure/Function Calls and the Stack

When variables are allocated on the stack, their actual addresses are not known at compile time, unlike the variables in the examples above where we could see their positions in the memory map. What this means, in general terms, is that the compiler generates code that references the stack-allocated variables by giving their position relative to a position in the stack pointed to by the frame pointer.

This has the effect of vastly simplifying the invocation of procedures and functions. To call a procedure you need to first remember where in the calling program to return back to. Then you need to pass it arguments. The procedure then needs to allocate its own local variables. And in the case of a function, the result needs to be returned. Finally the stack needs to cleaned up, and a jump made back to the calling code. All of this transient storage for the call comes from the stack, and is referred to as the stack frame

When you do a function call like
x = f(a, b, c);
the following happens: Note, the actual details and ordering of these actions depends on the compiler, and on the (linkage conventions used by the programming language and run-time system. But the general idea is the same for all procedural programming languages.

A very important consequence of this design is that functions and procedures can call themselves, that is, they can use recursion. We have already seen one important recursive algorithm, namely the one for computing the greatest common divisor (gcd) of two natural numbers.

code/gcd/gcd.ino

    int32_t gcd(int32_t a, int32_t b) {
      if (b == 0) {
        return a;
      }
      if (b == 1) {
        return 1;
      }
      return gcd(b, a % b);
    }
     
    void setup() {
      Serial.begin(9600);
      Serial.println( gcd(36, 24) );
    }
     
    void loop() {
    }


And we will see another when we do merge sort and quick sort in the coming sections.

10.8 Passing Arguments by Value to Procedures and Functions

We mentioned that making a procedure call like:
do_something(x, y, z);
does not pass the actual variables the procedure do_somthing. Instead only the current values of the variables are passed. The procedure has no idea where these values came from. This is called call by value.

This has major implications when we take some commonly used code and package it up as a procedure. Consider the following code. In the setup procedure below, the inline swap code cannot be simply turned into a procedure by wrapping it a procedure defintion.

code/Memory/Swap/swap.cpp

    #include <Arduino.h>
     
    // this does not modify the original arguments, since 
    // x and y are copies of the arguments
    void swap_int16_t_bad(int16_t x, int16_t y) {
        int16_t tmp = x;
        x = y;
        y = tmp;
        }
     
    // this does not modify the original arguments either,
    // since x_p and y_p are copies of the arguments.  However,
    // they are pointers to the variables that we want to swap
    // values for.
    void swap_int16_t(int16_t *x_p, int16_t *y_p) {
        int16_t tmp = *x_p;
        *x_p = *y_p;
        *y_p = tmp;
        }
     
    void setup() {
        Serial.begin(9600);
     
        int16_t a = 2;
        int16_t b = 3;
     
        Serial.print("a b are ");
        Serial.print(a);
        Serial.print(" ");
        Serial.println(b);
     
        // swap a and b with inline code
        {
            int16_t tmp = a;
            a = b;
            b = tmp;
            }
     
        Serial.print("a b are ");
        Serial.print(a);
        Serial.print(" ");
        Serial.println(b);
     
        // swap that doesn't work because only the values in 
        // a and b are passed
        swap_int16_t_bad(a, b);
     
        Serial.print("a b are ");
        Serial.print(a);
        Serial.print(" ");
        Serial.println(b);
     
        // swap that does work because we pass the address of 
        // the variables whose values need to be changed.
        swap_int16_t(&a, &b);
     
        Serial.print("a b are ");
        Serial.print(a);
        Serial.print(" ");
        Serial.println(b);
        }
     
    void loop() {
        }


When run it prints
a b are 2 3
a b are 3 2
a b are 3 2
a b are 2 3
which shows that unless you pass the address of a variable v to a procedure you will not be able to alter the contents of v.

Now when the procedure argument is an array or string (which is just an array in which the end is indicated by \0) then the variable associated with the array is already a pointer. This still means that the procedure cannot modify the argument (the pointer), but it can of course modify the array pointed to by the argument.

So for example, this is how we can write a swap procedure for two arrays of the same length:

code/Memory/SwapArray/swap_array.cpp

    #include <Arduino.h>
     
    // swap the contents of array x and y, both of length len
    void swap_int16_t_array(int16_t x[], int16_t y[], uint16_t len) {
        for (uint16_t i=0; i < len; i++) {
            int16_t tmp = x[i];
            x[i] = y[i];
            y[i] = tmp;
            }
        }
     
    void print_array(int16_t x[], uint16_t len) {
        for (uint16_t i=0; i < len; i++) {
            Serial.print(" ");
            Serial.print(x[i]);
            }
        }
     
    void setup() {
        Serial.begin(9600);
     
        int16_t len = 2;
        int16_t a[2] = {11, 22};
        int16_t b[2] = {33, 44};
     
        Serial.print("a");  print_array(a, len);  Serial.println();
        Serial.print("b");  print_array(b, len);  Serial.println();
     
        swap_int16_t_array(a, b, len);
        Serial.println("After swap");
        Serial.print("a");  print_array(a, len);  Serial.println();
        Serial.print("b");  print_array(b, len);  Serial.println();
        }
     
    void loop() {
        }


Which generates the output:
a 11 22
b 33 44
After swap
a 33 44
b 11 22


Exercise: Write this function:
// given array x of length len
// position i, 0 <= i < len
// a pointer to result r
// set r to the value of x[i]
void extract(int16_t x, uint16_t len, uint16_t i, int16_t *r)


10.9 Utilities for Managing Memory

mem_syms.h contains a number of macros that let you inspect the current state of the stack and heap to monitor your memory usage.

Variables that are allocated on the stack are compiler-managed. That is, the compiler takes care of placing them on the stack on entry to a block, and removing them on exit from the block. For this reason such variables are called automatic or local.

Variables on the heap are self-managed (or manual) variables. That is, the program must acquire memory for the variable by using malloc, and released by using free.

Another class of variables is garbage-collected. That is, when they are no longer needed, their memory is released. In a sense they are automatic variables allocated on the heap.

We can inspect the state of memory allocation using the macros defined in mem_syms.h We use macros instead of functions because we don't want the inspection to affect the amount of memory in use. Calling a function would allocate memory on the stack.

To use mem_syms.h, you can copy it into the directory with the file that you wish to include those macros. In your main file, you will also need to tell the complier to include the contents of the file. mem_syms.h. This file is also in the UAUtils lib.

code/Memory/mem_syms.h

    /*
      
      If you have the libc 1.6.4 libraries then free space at the end of the
      heap is NOT released.
     
      With the libc 1.7.1 libraries with the fixed malloc, then free space 
      at the end of the heap is released so that the space is available 
      to the stack or the heap.
      
      AVAIL_MEM is a macro to tell you how much available free memory is left
      between the top of stack and end of heap.  
     
      STACK_TOP is a macro to tell you the current address of the top of
      the stack.  There is data in this location ??
     
      STACK_BOTTOM is a macro to tell you the address of the bottom of the
      stack.  It should normally be constant.
     
      STACK_SIZE is a macro to tell you the current size of the stack
     
      HEAP_START is a macro to provide the address of the first location in
      the heap.
     
      HEAP_END is macro to provide the address of the current last location in
      in the heap.  This will increase as memory is allocated, and decrease if
      the end of the heap consists of free memory and can be shrunk.
     
      HEAP_SIZE is a macro to tell you the current size of the heap, but it
      includes chunks that are free.
     
      There should be a routine to detect the presence of the old or new malloc.
     
     */
     
    #ifndef mem_syms_h
    #define mem_syms_h
     
    #include "avr/io.h" 
     
    /* these symbols get us to the details of the stack and heap */
     
    // the address of the first byte of the stack, the last byte of onchip SRAM
    #define STACK_BOTTOM ( (char*)RAMEND )
     
    // the first free location at the top of the stack
    #define STACK_TOP ((char *)AVR_STACK_POINTER_REG)
      
    // the amount of stack used.  i.e. from bottom to top-1
    #define STACK_SIZE (STACK_BOTTOM - STACK_TOP)
     
    #define AVAIL_MEM (STACK_TOP - HEAP_END)
     
    // the address of the first location of the heap
    #define HEAP_START __malloc_heap_start
     
    // the address of the next free location after the end of the heap
    // __brkval is 0 until the first malloc
    #define HEAP_END (__brkval ? __brkval : __malloc_heap_start) 
     
    // the amount of space taken up by the heap, including free chunks
    // inside the heap
    #define HEAP_SIZE (HEAP_END - HEAP_START)
     
    extern char*__brkval;
     
    #endif


10.10 Other examples for your viewing pleasure



code/Memory/Mem04/Mem04.ino

    // show temporary variable allocation
    // remove var = initialization and location changes from 0x200 to 0x212
     
    void setup() {
      Serial.begin(9600);
      
      char var_1;  
      int16_t var_2;
      int16_t var_3;
      
      Serial.println("MEM04");
      
      Serial.println( (uint16_t) &var_1, HEX);
      Serial.println( (uint16_t) &var_2, HEX);
      Serial.println( (uint16_t) &var_3, HEX);
      Serial.println( (uint16_t) &var_3 + sizeof(var_3), HEX);
      
      Serial.println("i, j");
      for (int16_t i = 0; i < 2; i++) {
          char j = i;
          Serial.println( (uint16_t) &i, HEX);
          Serial.println( (uint16_t) &j, HEX);
      }
     
      Serial.println("k, l");
      for (int16_t k = 0; k < 2; k++) {
          int l;
          Serial.println( (uint16_t) &k, HEX);
          l = k + k;
          Serial.println( (uint16_t) &l, HEX);
      }
    }
     
    void loop() {
    }


So what is in the 10 locations from 0x21F6 to 0x21FF? It is the return information for setup, plus other things. The local variables i and j are also on the stack, but in non-adjacent positions.

code/Memory/Mem05/Mem05.ino

    // show temporary variable allocation
    #include "mem_syms.h"
    // remove initialization and location changes from 0x200 to 0x212
     
    // format for printing addresses, sometimes HEX, sometimes DEC
    #define ADDR DEC
    #define PRINT_ADDR(x) Serial.println( (int) x, ADDR)
     
    void setup() {
      Serial.begin(9600);
        
      Serial.println("MEM05");
     
      Serial.println("Stack bot, top, size");
      Serial.println( (int) STACK_BOTTOM, ADDR);
      Serial.println( (int) STACK_TOP, ADDR);
      Serial.println( (int) STACK_SIZE, ADDR);
      
      Serial.println("Avail");
      Serial.println( (int) AVAIL_MEM, ADDR);
     
      Serial.println("Heap start, end, size");
      Serial.println( (int) HEAP_START, ADDR);
      Serial.println( (int) HEAP_END, ADDR);
      Serial.println( (int) HEAP_SIZE, ADDR);
      char *c = (char *) malloc(10);
      Serial.println( (int) HEAP_END, ADDR);
      Serial.println( (int) HEAP_SIZE, ADDR);
     
     
      Serial.println( (int) STACK_TOP, ADDR);
      
      Serial.println("Avail");
      Serial.println( (int) AVAIL_MEM, ADDR);
      
      {
        char x;
        Serial.println( (int) &x, ADDR);
        Serial.println( (int) STACK_TOP, ADDR);
      }
      Serial.println( (int) STACK_TOP, ADDR);
    }
     
    void loop() {
    }


code/Memory/Mem06/Mem06.ino

    // What is used by a function call?
     
    #include "mem_syms.h"
    // remove initialization and location changes from 0x200 to 0x212
     
    // format for printing addresses, sometimes HEX, sometimes DEC
    #define ADDR DEC
    #define PRINT_ADDR(x) Serial.println( (int) x, ADDR)
     
    char* stack_before;
    char* stack_after;
    int stack_used_by_call;
     
    void fn() {
      int x = 42;
      stack_after = STACK_TOP;
      // do something
      Serial.print("fn ");
      Serial.println(x, DEC);
    }
     
    void setup() {
      Serial.begin(9600);
        
      Serial.println("MEM06");
     
      Serial.println("Stack bot, top, size");
      Serial.println( (int) STACK_BOTTOM, ADDR);
      Serial.println( (int) STACK_TOP, ADDR);
      Serial.println( (int) STACK_SIZE, ADDR);
      
      stack_before = STACK_TOP;
      fn();
      
     
      stack_used_by_call = (int) stack_before - (int) stack_after;
     
      Serial.print("Stack used ");
      Serial.println(stack_used_by_call, ADDR); 
      
      Serial.println("Stack bot, top, size");
      Serial.println( (int) STACK_BOTTOM, ADDR);
      Serial.println( (int) STACK_TOP, ADDR);
      Serial.println( (int) STACK_SIZE, ADDR);
      
     }
     
    void loop() {
    }


code/Memory/Mem07/Mem07.ino

    // What is used by a function call?
     
    #include "mem_syms.h"
    #define ADDR DEC
     
    unsigned int gcd(unsigned int a, unsigned int b) {
      unsigned int result;
      
      Serial.print("Stack size: ");
      Serial.println( (int) STACK_SIZE, ADDR);
     
      if ( a == 0 ) { 
        result = b;
      }
      else if ( a == 1 ) { 
        result = 1;
      }
      else {
        result = gcd(b, a % b);
      }
      
      // comment out the next two lines and observe the change in stack size
      Serial.print(a, DEC);  Serial.print(" ");  
      Serial.print(b, DEC);  Serial.println(" ");
      return result;
    }
      
     
    void setup() {
      unsigned int g;
      Serial.begin(9600);
        
      Serial.println("MEM07");
      
      Serial.print("Avail mem:");  Serial.println(AVAIL_MEM, DEC);
     
      g = gcd(8820, 462);  
      Serial.print("gcd is ");
      Serial.println(g, DEC);  
     }
     
    void loop() {
    }

10. Memory
Tangible Computing / Version 3.20 2013-03-25