Uploading the Program on Our Own

Last time, I started implementing a Rust application to upload our program to the microcontroller. Today, we're going to wrap this little detour up by finishing the uploader and using it to upload our program!

I've already presented the infrastructure I built in order to talk to the SAM-BA bootloader and the flash controller (EEFC). It remains mostly unchanged. The only relevant thing I've added is a new command:

pub struct ErasePageAndWritePage;

impl Command for ErasePageAndWritePage {
    type Argument = Page;

    fn value() -> u8 { 0x03 }

pub struct Page(pub u16);

impl Argument for Page {
    fn value(self) -> u16 {
        let Page(page) = self;

This command, as the name implies, erases a page and then overwrites it with new data[1]. The page is identified by a 16-bit unsigned integer.

We can use this new command to upload a file into flash memory. Let's start with setting some variables we're going to need for that:

let flash_base_addr = 0x00080000;

let word_size_bytes = 4;
let page_size_bytes = 256;
let page_size_words = page_size_bytes / word_size_bytes;

let number_of_pages =
    (file_size + page_size_bytes - 1) / page_size_bytes;

All of this is pretty straight-forward. The flash memory starts at address 0x00080000, and that's where we're going to write the program later.[2] In addition to that, we need some basic values. The size of a word is 4 bytes, since the SAM3X8E is a 32-bit architecture.[3] The flash memory is divided into 1024 pages, each consisting of 256 bytes.[4]. Lastly, we compute the number of pages we need to write from the size of the file.

sam_ba.write_word(0x400E0A00, 0x00000600)
    .expect("Failed to write wait state");

Here's a bit of code I'm not sure we actually need. According to the errata section in the data sheet[5], we need to set the number of wait states to 6. Otherwise "the data may not be correctly written". Setting the wait states to 6 means that flash read/write operations take 7 CPU cycles. We're setting the wait states here by writing to the Flash Mode Register (FMR) of the Enhanced Embedded Flash Controller (EEFC).[6]

I actually wasn't able to verify that this is actually needed. When I tested with different values, the uploaded program still worked correctly. However, I'm not sure how conclusive this test is. I had a bug during development that caused some data from the file not to be uploaded, and the program still worked correctly. I suspect this is because the program isn't optimized as yet. Anyway, I've left this piece of code in, even though we might not need it. Better safe than sorry.

for page in 0 .. number_of_pages {
    for i in 0 .. page_size_words {
        let offset  = page * page_size_bytes + i * word_size_bytes;
        let address = flash_base_addr + offset;

        let word = if offset < file_size {
                .expect("Failed to read from file")
        else {

        sam_ba.write_word(address, word)
            .expect("Failed to write word");

        .execute_command::<ErasePageAndWritePage, _>(
            &mut sam_ba,
            Page(page as u16),
        .expect("Failed erase page and write page");

Here we get to the meat of the upload. We loop over all pages we need to write, and within that loop over all words of the page. We read that word from the file and write it to the flash memory. Since the file size will not necessarily be a multiple of the 256-byte page size, we need to take into account that we can't read all bytes of the last page from the file.

After we've written a full page, we need execute a flash command to actually write it to flash memory.[7] This is because writing the words to the address will actually write them to a buffer, not the flash memory directly. That buffer is as big as a page, and writing to any flash memory address actually writes to that same buffer.[8] That means, we need to write the page to flash before we start writing the next page. Otherwise we would overwrite the values in the buffer.[9]

    .execute_command::<SetGpnvmBit, _>(
        &mut sam_ba,
    .expect("Failed to set GPNVM bit");

Finally, we need to select the boot mode, so the microcontroller will boot our program after we reset it. The details of this were covered in the last article.

So, with all this, we can finally upload our program to the microcontroller, without relying on BOSSA. The last thing we need to do is integrate the new uploader into our compile/upload script.


./compile &&

arm-none-eabi-objcopy \
    -O binary \
    output/blink.elf \
    output/blink.bin &&

    cd uploader
    cargo run -- $DEVICE upload-file ../output/blink.bin)

Before you can use this, you may need to adapt the DEVICE variable to your system.

That's it for today! As always, the full code is available on GitHub. I hope you will join me again next time. With the uploader working, we can finally get back to improving our LED blinking program.