2015-11-03

Telling the Microcontroller to Boot from Flash

In the previous article, we experimented with talking to the SAM-BA bootloader directly. There already was a first success in getting the bootloader to print its version, without the use of any intermediary program.

That gave me the idea to write an uploader in Rust to upload the program to the microcontroller. After all, how hard could it be? We have documentation on how to talk to the bootloader[1] and the source code for BOSSA. Shouldn't that be enough? Well, as it turns out, some experimentation (and some plain old banging-my-head-against-the-wall) was still required. But the results so far are looking good!

Before I get into that though, let's address the "why" first. Why would I write an uploader myself instead of using something existing, like the official SAM-BA In-system Programmer by Atmel? Well, as I explained last time, the previous solution, BOSSA, stopped working for me for unknown reasons and Atmel's SAM-BA doesn't really fit my workflow.

While I don't suppose I could write a generally better replacement for either of those, I can certainly come up with something that works better for my specific use case. And let's not forget about the main goal of this endeavor so far: To learn more about embedded programming. Writing this uploader allows us to see the microcontroller from a different perspective than usual.

But enough with the introduction, let's get into the uploader's source code[2]!

fn main() {
	// Parsing of command-line arguments not shown here.

	let     port   = init_port(&device_path).unwrap();
	let mut sam_ba = SamBa::new(port);
	let     eefc_0 = Eefc::eefc_0();

	// Here comes some more initialization and the command execution code. See
	// below.
}
			

That's the rough outline of the initialization code. First, we initialize a serial port by calling init_port. The device_path is passed in by the user as a command-line argument[3]. Then we create a SamBa instance, which is our interface to the SAM-BA bootloader. We also create an instance of Eefc, which we'll use to talk to the Enhanced Embedded Flash Controller (EEFC) that's part of the microcontroller[4], specifically to the one that controls the first flash memory bank, of which the SAM3X8E has two[5].

I'll get into some more details later, but let's continue with the rough outline first.

fn main() {
	// Argument parsing and the previously shown initialization code belong
	// here.

	sam_ba.set_normal_mode().unwrap();

	// Command execution fits here. See below.
}
			

This, unsurprisingly, sets SAM-BA to normal mode, which means it will send answers in binary. The alternative would be terminal mode, which sends answers as text. Normal mode seemed like it was easier to work with, so I went with it.

fn main() {
	// Initialization code not shown here. See above.

	match command.as_ref() {
		"version" =>
			print!("{}", sam_ba.display_version().unwrap()),
		"boot-mode" => {
			let result =
				eefc_0.execute_command::<GetGpnvmBit, _>(
					&mut sam_ba,
					GpnvmNumber::BootModeSelection,
				)
				.unwrap();

			print!("{:0>8x}\n", result)
		},
		"boot-from-flash" => {
			eefc_0.execute_command::<SetGpnvmBit, _>(
				&mut sam_ba,
				GpnvmNumber::BootModeSelection,
			)
			.unwrap();
		},
		_ =>
			print!("Unknown command: {}\n", command),
	}
}
			

Here we're executing a command that has been passed by the user via the command-line. version tells SAM-BA to return its version. boot-mode will print the value that controls what memory the microcontroller boots from, and boot-from-flash will tell it to boot from flash memory next time it's powered up.

That's basically all it does for now. Easy, isn't it? From here on out, we'll taker a closer look at the important steps, but I won't show off every single line of code. Please check out the full code on GitHub, if you're interested in all the details.

Let's start with the initialization of the serial port.

fn init_port(path: &str) -> serial::Result<serial::SystemPort> {
	let mut port = serial::open(path).unwrap();

	try!(port.reconfigure(&|settings| {
		try!(settings.set_baud_rate(serial::Baud115200));
		settings.set_char_size(serial::Bits8);
		settings.set_parity(serial::ParityNone);
		settings.set_stop_bits(serial::Stop1);

		Ok(())
	}));

	Ok(port)
}
			

This uses the excellent serial library to initialize the port in the correct manner[6]. If you have some basic knowledge about serial ports, this should be self-explanatory. Explaining all about serial ports is unfortunately out of the scope of this article.

Next up, our interface to the SAM-BA bootloader.

pub struct SamBa {
	port: serial::SystemPort,
}

impl SamBa {
	// Some less interesting code has been omitted.

	pub fn read_word(&mut self, address: u32) -> Result<u32> {
		try!(write!(self.port, "w{:0>8X},#", address));
		let result = try!(self.port.read_u32::<LittleEndian>());

		Ok(result)
	}

	pub fn write_word(&mut self, address: u32, value: u32) -> Result<()> {
		try!(write!(self.port, "W{:0>8X},{:0>8X}#", address, value));
		Ok(())
	}
}
			

The most important function of SAM-BA is letting us to read from and write to memory. It has several commands to do this, but for now we just need to read and write whole words (which are 32 bits wide). Writing is pretty straight-forward, using the I/O and formatting facilities from Rust's standard library. To read the answer, we're using the (also quite excellent) byteorder crate.

Being able to directly access the memory on the microcontroller has obvious applications (we're trying to upload a program, after all), but think about it for a moment: As we've learned previously, the ARM architecture uses memory-mapped I/O, meaning that doing just about anything with the hardware takes the form of reading from and writing to memory addresses. SAM-BA lets us do this from a connected PC, which theoretically means we can control the microcontroller remotely.

We'll do just that in a moment, to tell it to boot from flash memory, but I'm sure there are interesting applications beyond that.

Let's continue with the interface to the EEFC.

pub struct Eefc {
	command_register: u32,
	status_register : u32,
	result_register : u32,
}

impl Eefc {
	pub fn eefc_0() -> Eefc {
		let base = 0x400e0a00;

		Eefc {
			command_register: base + 0x04,
			status_register : base + 0x08,
			result_register : base + 0x0c,
		}
	}

	// Some more code belongs here. We'll take a look at it below.
}
			

Here we set up the addresses for the EEFC controller. EEFC has three registers that are relevant to us. We'll write commands to the command register (called EEFC_FCR), read their results from the result register (EEFC_FRR), and read the general status from the status register (EEFC_FSR).[7]

This is similar to the approach we've been using previously to talk to hardware in our blink program. The difference is, running directly on the device, we can use a pointer to a struct to access the various. Here, we'll need to go through SAM-BA as an intermediary, so we'll need to handle the addresses of the registers directly.

impl Eefc {
	// The code shown above belongs here.

	pub fn execute_command<C, A>(&self, sam_ba: &mut SamBa, argument: A)
		-> Result<u32>
		where
			C: Command<Argument=A>,
			A: Argument,
	{
		let command =
			0x5a << 24
			| (argument.value() as u32) << 8
			| C::value() as u32;

		try!(sam_ba.write_word(self.command_register, command));

		while try!(sam_ba.read_word(self.status_register)) & 1 == 0 {}
		assert_eq!(try!(sam_ba.read_word(self.status_register)), 1);

		sam_ba.read_word(self.result_register)
	}
}
			

As the name suggests, this is the method that executes the commands the EEFC understands. Each command is executed in the same way (as implemented here), but there are different commands that take different kinds of arguments. This method uses some type system magic to make itself idiot-proof[8], meaning the user is prevented at compile time from passing an invalid combination of command and argument[9].

Other than that, it's pretty straight-forward:

I promise, we're almost done. One of the last things we need to know about are the Command and Argument traits.

trait Command {
	type Argument: Argument;

	fn value() -> u8;
}

trait Argument {
	fn value(self) -> u16;
}
			

Not much to see here. A command has an associated argument type and an 8-bit value that identifies it. An argument can be encoded as a 16-bit value.

And the last bit before we actually get to use the program: The implementation of the actual commands.

pub struct SetGpnvmBit;

impl Command for SetGpnvmBit {
	type Argument = GpnvmNumber;

	fn value() -> u8 { 0x0b }
}


pub struct GetGpnvmBit;

impl Command for GetGpnvmBit {
	type Argument = GpnvmNumber;

	fn value() -> u8 { 0x0d }
}


pub enum GpnvmNumber {
	// Security          = 0,
	BootModeSelection = 1,
	// FlashSelection    = 2,
}

impl Argument for GpnvmNumber {
	fn value(self) -> u16 { self as u16 }
}
			

For now we just need two commands: One for setting a GPNVM bit and one for reading its value. There are three GPNVM bits, but we need only one right now: The one that controls what memory the microcontroller boots from. That can be either ROM (where the bootloader lives) or Flash (where our program lives).[13]

As a reminder, here's how we use this infrastructure to set the boot mode selection bit, i.e. tell the microcontroller to boot from flash on the next reset:

eefc_0.execute_command::<SetGpnvmBit, _>(
	&mut sam_ba,
	GpnvmNumber::BootModeSelection,
)
			

So, on to the home stretch. Let's use the program to make the microcontroller boot from flash! If we connect the board to a PC via USB on the native port (that's the one closer to the RESET button) and erase the flash memory by pressing the ERASE button for a moment, that should not only clear the flash memory, but also start the bootloader. We can confirm this (on Linux) by running lsusb. There should be an entry for the board that looks somewhat like this:

Bus 003 Device 004: ID 03eb:6124 Atmel Corp. at91sam SAMBA bootloader
			

Now we can switch to the uploader directory and build and run it like this, to get the current value of the boot mode selection bit[14]:

$ cargo run -- /dev/ttyACM0 boot-mode
			

The device path might be different on your machine (like /dev/ttyACM1, /dev/ttyACM2 or similar). If you have multiple device files, just try all of them until one works without error.

If everything worked, the output should look like this: 00000000

This means the boot mode selection bit is 0, and the device boots from ROM. Only makes sense, because if it hadn't booted from ROM, the bootloader wouldn't be running and we wouldn't be talking to it.

Next, let's tell it to boot from flash memory:

$ cargo run -- /dev/ttyACM0 boot-from-flash
			

Now, we can ask for the boot selection bit again:

$ cargo run -- /dev/ttyACM0 boot-mode
			

The result should look like this: 00000002. This shows all the GPNVM bits, and the boot mode selection bit is the second least significant one. A value of 2 means the boot mode selection bit is set.

Now for the final step: Press the RESET button for a moment and run lsusb again. The entry we previously saw should no longer be there. Why? Well, we told the microcontroller to boot from flash memory, but didn't upload a program. This means no program is currently running on the device, so there is nothing that controls the USB port.

You can start over by pressing ERASE, to get back to the bootloader.

Phew, that was quite a bit of ground that we covered today! As always, the full code is available on GitHub. Tune in next time, when we will hopefully build on today's foundation and actually upload a program.