Monday, January 28, 2013

An Intro to Hardware Hacking for CS folk: Part 2, How the code works

(N.B. This post tends to be a bit more about how things work instead of how to use them. For Arduino/microcontroller quick starts, see Instructables or Adafruit)

The last post (a really long time ago) talked about what a microcontroller was and its purpose. This post aims to take the next step of introducing how software development works for these devices. I'll begin with what any proper CS blog post should, a language discussion.

What language do I use?


Microcontroller programming is typically done in C or C++, and assembly is not uncommon. There are a few reasons why these lower level languages are de rigeur:
  • Resource consumption - Going back to the previous post on microcontroller specs, a few KB of program memory does not allow for managed runtimes. Usually the only code on the chip are the routines that the programmer developed or explicitly linked against (I say usually because operating systems and bootloaders for microcontrollers do exist, and perhaps I'll discuss them later). Even if the program memory did exist, microcontrollers typically do not have the cycles to spare for garbage collection or handling unchecked runtime exceptions. The goal is to have just enough code to do the job. As microcontrollers gain more hardware resources, the use of languages like C++ grows as the overhead of object-oriented programming can be mitigated by increased developer productivity.
  • Determinism - Microcontrollers are often used in mission critical situations. Couple that with the hardware resource constraints mentioned above, and embedded systems developers become wary of every cycle spent in a routine. Many often used functions are written in assembly to ensure any time or space constraints are met, and that they are met consistently.
  • Portability - While in the PC world software is almost exclusively written for x86, and in the mobile world software is almost exclusively written for ARM, in the embedded devices landscape there are numerous ISAs (AVR, ARM Thumb, m68k, Renesas RX, PIC, TI MSP430, and many many more). Chances are there is a C/C++ compiler available to help keep your software ISA-agnostic.
  • Toolchain - C and C++ undoubtedly have some of the most mature software toolchains. This is especially important for embedded software, where compiler optimizations are a necessary tool for reducing code size but cannot endanger software correctness. On the latter note, there are also many lint tools available for C.
Now that we have some basis for why the code is in asm/C/C++, let's start looking into the actual code itself.

What does the code look like?


At the minimum, it is something like this:

void main()
{
    while( 1 );
}

To readers that are used to writing scripts or non-REPL shell applications, the mandatory infinite loop seems a bit weird. The reason for its existence is simple: there's no other code to execute other than your own. If there wasn't an infinite loop, the program counter would just keep stepping through some undefined or zeroed-out code memory until it hits something ugly (like interrupt vectors, but that's another post down the road). Regardless, more than likely the job of your microcontroller program will be to constantly respond to input. Typically then we can segment the code into two parts: one for setting up the hardware for receiving that sensory input, and the other the loop to continuously process and act on it. Conveniently, the Arduino framework separates this out for us. Here's what a blank Arduino sketch (fancy word for program) would look like:

//// Copied from the Arduino BareMinimum Example

void setup() {
  // put your setup code here, to run once:

}

void loop() {
  // put your main code here, to run repeatedly: 
  
}

As a teaser, here's an example program showing how those functions could be filled out:

//// Copied from the Arduino DigitalReadSerial Example

/*
  DigitalReadSerial
 Reads a digital input on pin 2, prints the result to the serial monitor 
 
 This example code is in the public domain.
 */

// digital pin 2 has a pushbutton attached to it. Give it a name:
int pushButton = 2;

// the setup routine runs once when you press reset:
void setup() {
  // initialize serial communication at 9600 bits per second:
  Serial.begin(9600);
  // make the pushbutton's pin an input:
  pinMode(pushButton, INPUT);
}

// the loop routine runs over and over again forever:
void loop() {
  // read the input pin:
  int buttonState = digitalRead(pushButton);
  // print out the state of the button:
  Serial.println(buttonState);
  delay(1);        // delay in between reads for stability
}

There is a lot left to cover though to explain how that program works. One last piece I want to explain in this post is how does the microcontroller load the program in the first place.

Where does the code go?


On a general purpose computer, executing a program is rather complicated. The program has to be loaded from disk, its address space allocated, and its code and globals copied into memory. Since the microcontroller is generally a single purpose device, we don't have to worry about resource allocation. Our principal concern is how to get a binary we compiled on a PC to executing on a microcontroller. Our analogue to a disk is the EEPROM, and to get the code there we need a programmer.

The EEPROM, also called Flash memory since it uses the same floating-gate transistor technology as your SSD or your smartphone storage, is programmable read-only memory that can be erased electrically (ta-da!). In the case of the Arduino Uno, the Atmel AVR ATmega328P microcontroller onboard has 32KB of Flash memory (Atmel distinguishes Flash as the program storage area, and EEPROM as a smaller buffer for the programmer to store non-volatile variables, but the underlying technology should be the same and I'll use the terms interchangeably in this post). 32KB is a surprisingly large amount of space when you have the only program; enough for even autopilot software with GPS support. Since it is non-volatile, cutting the power to the microcontroller will not erase the program. The Flash memory on the ATmega328P is good for 10,000 writes, so feel free to keep "flashing."

To actually write to the EEPROM, a programmer is typically required. This procedure can get complicated, since we have to transfer a binary file from a PC with typically only USB as I/O to an EEPROM using its write commands. If we just had an AVR microcontroller we would have to buy a programmer (an AVR-ISP in this case, since it would use AVR's in-system programming protocol). Luckily, the combination of the Arduino board and software simplifies this process through their bootloader. The Arduino bootloader is a small program permanently resident in EEPROM that runs on microcontroller reset. It receives data from the PC through the Arduino IDE (and through a USB to serial chip, will discuss later), writes it to the EEPROM, and then starts the written program. The mbed microcontroller also has a similar mechanism, and cleverly exposes itself as a USB Mass Storage Device so that you can drag-n-drop the program binary onto the device. Once the EEPROM is loaded, execution starts, and continues ad infinitum (unless you forgot that infinite loop!)

Where do we go from here?


So far this post has been about code, but not what to code. The next post will be about I/O, which will be the basis for all interactions with the microcontroller. Any feedback in the meanwhile is definitely appreciated!

No comments:

Post a Comment