In my previous post I alluded to some work I was doing on a bootloader for the ATtiny (and other chips). This has been progressing well so it seemed like a good time to start providing more details. This project is still a work in progress so nothing is completely finalised yet but any changes are going to be fairly minimal.
This work came out of two sets of requirements that wound up aligning with each other - the first was my desire for a bootloader to ease the development of some more ATtiny based projects, the second was to develop a bootloader for the prototyping boards I've been working on.
The Microboard system is designed to support a range of different MCU's - some of which have built in bootloaders (like the LPC1114), some that have well defined bootloaders and support tools available for them (like the ATmega series) and others that have no standardised bootloader protocol (like the PIC16F1827 and the PIC32MX series). I had already decided to write my own bootloader for Microchip PIC devices and, after some thought, it seemed like a good idea to implement this on the AVR chips as well rather than wind up with a different bootloader and set of tools for each chip. The end result will be a common bootloader protocol and tools for all chips except the LPC1114 (the built in bootloader is fine - implementing my own would just be redundant).
Because the AVR chips are so widely used there is no shortage of example code to refer to - it was the best target to start with. With a bit of work the same bootloader could be made to work on the ATtiny chips as well so it solves two problems at the same time. This is the route I have been heading down over the past few weeks.
One major goal I have to keep the bootloader as simple and as straight forward as possible - it's not meant to be the fastest or smallest available, just easy to use and easy to understand. The implementation comes in at less than 1K on any target platform and will communicate at 57.6 KBaud - fast enough for daily use without trying to break any speed records. It is not required to modify the EEPROM or fuse bytes on chips that have them, it's only purpose is to modify the program flash.
The remainder of this post documents the behaviour of the bootloader, the protocol used for communication and describes some of the utilities I've developed to work with it. Implementation details for specific processors will be described in future posts.
Entering the Bootloader
The bootloader is run whenever the MCU resets, at this stage it determines if it should enter bootloader mode or continue to the application program currently on the flash. The first versions I worked on initialised the serial port of the MCU and waiting for a certain period for any communications, if nothing arrived the application was started. This approach turned out to be very annoying - it caused an unnecessary delay on each power cycle and when you did want to transfer code it could be very difficult to start the communications within the window of opportunity available. It also put restrictions on what hardware you could attach to the serial pins - if it generated any input to the MCU the bootloader mode was entered - leaving you wondering why the application wasn't doing what you expected.
The current model is based on the behaviour of the LPC bootloader - a bootloader entry pin is checked, and if it is low the serial port is initialised and the bootloader code started. If it is not low the application is started immediately. Where possible the implementation uses an IO pin that has an internal weak pull-up resistor - eliminating the need for any external circuitry. There are still some restrictions on the use of that pin from the application code (it can't be low at reset for example) but they can be worked around very easily.
Once the bootloader is started it will configure the serial port for a baud rate of 57.6 KBaud, 8 bits per character, 1 stop bit and no parity (8N1). It will then listen on the serial port for incoming commands.
I've deliberately kept the protocol very simple to make it easier to process on the chip and easy to write host side tools to work with it. All information is transferred in printable ASCII characters with binary data in hexadecimal, this means that more data is transferred but makes it a lot simpler to debug. It also means that you can interact with the boot loader manually, dynamically writing data to arbitrary locations in memory.
Each packet starts with a command (when sent from the PC to the MCU) or a status indicator (from the MCU to the PC) which is optionally followed by a sequence of data bytes and finally terminated with the 'line feed' character. When data is sent it is a sequence of byte values in hexadecimal format. The data is verified with a 16 bit checksum which is the sum of all the individual bytes added to the seed value 0x5050 with overflow ignored. The Python code to generate the checksum for a list of values looks like this:
checksum = 0x5050 for val in data: checksum = (checksum + (val & 0xFF)) & 0xFFFF
There are four supported commands sent from the PC to the MCU. Commands can be sent in any order, no command depends on the result of another. Responses are always prefixed with the '+' character (to indicate success) or '-' to indicate failure. The description of each command is as follows ...
This is generally the first command sent, it reports various information about the bootloader and can be used to determine that you are in fact talking to the bootloader. The format is simply the '?' character immediately followed by the new line character. The response is a list of byte values followed by a checksum. A typical transaction looks like:
> ? < +101001015072
The response consists of four data bytes and a two byte checksum. The data bytes are:
|0||Protocol version. The current version is 1.0 and represented as 0x10 hex.|
|1||The size of each data line. This tells the host how many bytes of data should be placed in each write command (and how many bytes will be returned for a read command).|
|2||This value indicates the type of CPU - AVR, PIC or PIC32|
|3||The specific model of the CPU - ATmega8, ATtiny85, etc|
The read command is used to read the contents of the flash memory on the MCU and is expressed as the letter 'R', a 16 bit address in 4 hex digits and a 16 bit checksum. If the read is successful the return value will be the '+' character, a 16 bit address as 4 hex digits, a sequence of bytes where each byte is a two character hex value, the 16 bit checksum and finally the terminating new line character.
A typical sequence looks like the following:
> R00105060 < +00100EC00DC00CC00BC00AC009C008C011245622
The number of data bytes returned will be the value indicated by the query command.
The write command is used to change the contents of the flash memory on the MCU and is expressed as the letter 'W', a 16 bit address in 4 hex digits, a sequence of bytes where each byte is a two character hex value, the 16 bit checksum and finally the terminating new line character. The number of data bytes in the command must be equal to the value indicated by the query command. If there is not enough data or too little the command will fail.
A typical sequence looks like the following:
> W0000FFCD15C014C013C012C011C010C00FC057DA < +
The response to this command is always a simple success ('+') or fail ('-') acknowledgement followed by the new line character with no additional data.
The execute command causes the application program to be started. This can be used directly after uploading to start running the new application.
The command is represented by the exclamation mark character ('!') followed by a new line character. There is no response to this command as the bootloader will transfer control to the application and not return.
A bootloader by itself is not very useful without the corresponding utilities to run on the PC to use it. For this bootloader I have written some basic utilities in Python to transfer data to and from the device. A basic description of these tools follows.
Reading the Flash - mbdump.py
The mbdump.py utility will read the entire contents of the flash memory from the chip and save it to an Intel HEX format file. The usage information for this utility is ...
mbdump.py - Microboot/Microboard Firmware Dump Utility Copyright (c) 2014, The Garage Lab. All Rights Reserved. Usage: mbdump.py options [filename] Options: -d,--device name Specify the expected device, eg: attiny85,atmega8. This is required. -p,--port name Specify the name of the serial port to use for communication. If not specified the port /dev/ttyUSB0 will be used. --log Log all communications to the file 'transfer.log' If a filename is not specified the output will be saved in the file 'device'.hex, eg atmega8.hex if the device is an atmega8.
Writing Code - mbflash.py
The mbflash.py utility will read an Intel HEX file and program the flash on the MCU with it's contents. The usage information for this utility is ...
mbflash.py - Microboot/Microboard System Flashing Utility Copyright (c) 2014, The Garage Lab. All Rights Reserved. Usage: mbflash.py options filename Options: -d,--device name Specify the expected device, eg: attiny85,atmega8. This is required. -p,--port name Specify the name of the serial port to use for communication. If not specified the port /dev/ttyUSB0 will be used. --log Log all communications to the file 'transfer.log'
A simple example would be ...
:::shell $ ./mbflash.py -d attiny85 blinky.hex
This code would program an ATtiny85 connected on port /dev/ttyUSB0 with the contents of the file blinky.hex.
The Support Library - microboot.py
Both of the utilities above use a common support library contained in the file microboot.py. I've designed this to be easily integrated into other applications if custom firmware updating is required (for example - combining a fixed firmware blob with some use generated binary data).
I've just completed the first version of the bootloader running on the ATtiny85 using the same single pin serial interface I used in the safety light project and I've built a small breakout board with the additional circuitry required for the serial interface. I'm going to spend a few more days testing the implementation before I push the repository to GitHub for public access. There will be a few more posts over the next few days giving more details of that specific implementation.