Have you ever looked inside a hex file that you've downloaded from an online editor like MakeCode MakeCode or MicroPython and wondered how all those letters and numbers are translated into blinking LEDs on your micro:bit? Let's look at the actual code to better understand what's going on.
When a coder presses "Download" in an editor, the user-written script gets compiled and automatically bundled with the runtime (also called the "DAL", or Device Abstraction Layer) and the file is saved as a series of hexadecimal characters. Some compilers like MakeCode also append metadata like the website URL (encoded in hexadecimal) to the end of a hex file.
Regardless of whether the code is the runtime, a script, or metadata, each line is composed of the same 6 pieces of information:
All characters except the colon are in hexadecimal notation. If you are unfamiliar with hexadecimal, imagine if single digits could represent values of up to 15. The letters A-F are used for the higher values 10-15. So the decimal value 10 would be A, and the hexadecimal value F would be decimal 15. Adding one more value to hexadecimal F results in hexadecimal 10. Just as decimal 255 represents (2 x 100 + 5 x 10 + 5 x 1 ), hexadecimal FF is F x 16 + F x 1 (or 15 x 16 + 15 x 1).
Fig. 1 shows the first three lines of the hex code from the default OutOfBoxExperience-v2 file viewed using Notepad++. If you have flashed a new program to your micro:bit (very likely) and you want to look at the full code of this default program, it is available here: https://microbit.org/get-started/user-guide/out-of-box-experience/
Fig. 2 is the second line of this script that we will analyse in detail. It is the most typical sort of code and carries data values that get written to memory addresses.
Starting with the leftmost piece of data, we see the following pieces of code:
- The colon: indicates the start of a new line.
- The byte count indicates the number of hex digit pairs of data in this line of code. This line has 10. Remember all numbers are in a hexadecimal form which equates to 16 pairs of digits. One pair of digits represents an 8-bit byte ranging from 00-FF in hexadecimal (or 0-255 in decimal).
- The starting memory location that the first byte will be written to is next. As there are 16 pieces of data to write, this data will be written from address 0000 to 000F. According to Line 3 in Figure 1, the next data block will be written to memory addresses starting with hexadecimal 0010. This leaves room for the 16 bytes in the first line of data.
- Record type 00 refers to data values. Most lines will be of this type. Other record types indicate the end of the file or extended addresses.
- The data C0070000D1060000D1000000B1060000 will be written into 16 contiguous memory addresses, starting with address 0000 and ending with address 000F. Each data pair represents one value that will be written into one location in memory.
- Last is the checksum CA. The checksum is a quick way to verify that each line was read correctly. Imagine counting hundreds of 1p, 2p, and 5p coins to get a total and then having a partner doublecheck your tally. If you counted a total of £ 231.66, you could state the number of 1p, 2p, and 5p counted and hope your partner got the same amount. But instead, you could send the checksum value of 66 -- the smallest units of your total. It is unlikely that in counting 1p, 2p, and 5p coins that you would be off by a whole pound or even ten or 100 pounds, so matching the smallest digits is usually good enough. The same principle is used with checksums, but they use slightly more complicated calculations.
To calculate the checksum, add all the hex pairs in a line (except the checksum). From the line in Fig 2, this means adding the following numbers together:
The sum of these numbers is 336 in hexadecimal notation. Take the smallest two digits -- 36 -- and convert it to binary 0011 0110. Now invert all the binary digits, switching each 0 to a 1 and vice versa. (The fancy way to do this is by using an XOR operator with the binary number 1111 1111, but for small numbers, it's easier for humans to just flip the digits.)
The inverse of this value is 1100 1001. Converting back to hexadecimal gives the value C9. Now add 1 to this value for the final checksum of CA.
When the micro:bit receives its data it checks that the data matches the checksum. To do this, all the bytes, including the checksum are added together and the smallest digits should add up 00. If that is the case, the data is valid. For Line 2, hexadecimal 336 + CA = 00, so we know the data is correct.
Now comes the fun part. If we manipulate the hex file directly, we can write our own code using hexadecimal notation.
Below is a sample script is written in MicroPython in the online editor at python. microbit.org:
After downloading the hex file and opening it up in a text editor like Notepad++, I could find the code I had written. Near the end of the file are the following lines:
The line highlighted in purple has data from the script. Of course, I couldn't recognise the code I had typed just by scanning thousands of lines of hexadecimal code. So I stripped the code of colons, byte counts, addresses, record types, and checksums, leaving only the data. Using an online ASCII converter, I was able to quickly find the line of code I was looking for.
Going back to line 14484 in Notepad++, I altered the second pair of data values (representing the "H" in "Hello, World!") from 48 to 49, and changed the checksum appropriately from 54 to 53.
Now when I save the altered hex file and flash it to the micro:bit, it displays the odd message "Iello, World!" instead of "Hello, World!"
And there you have it, I hope you found this article interesting and learnt more about what makes up the hex file you drag onto your micro:bit to run a program!