In my previous post, we discussed about Unicode support in microcontroller-based devices in general. Now I will describe how the Unicode support is implemented in this project. Please make sure to check out the following source code files in order to understand better the description below:
- files in fonts directory
Tweets are received from Twitter REST and Streaming APIs. HTTPS packet payload contains JSON data. Text inside this data is Unicode-encoded and escaped in the following way: \uhhhh (where hhhh is a Unicode code point in hexadecimal format). So e.g. the letter 'Ä' will be presented as \u00c4 and the Cyrillic letter 'Я' will be \u042f.
JSON parser from Contiki OS is used to parse the text out of the JSON data. This parser, however, knows nothing about Unicode escape sequences, so I need to parse and decode them myself. Basically, I search for the string starting with '\u' and followed by the four ASCII-encoded hex digits. I then convert the hex digits into 16-bit value, which represents one character. Letters with Unicode code point value > 0xFFFF are not supported, but that is not a big issue in this project.
Bitmap Font Generator is used to generate a PNG image, which contains all the font block letters. Additionally, an XML description file is generated, which contains information for each letter (position, size, etc.). Those two files are then passed into my font converter application. I will publish the source code for this app on GitHub soon. Font converter generates a C-language array containing bitmap representation and meta info for each letter.
The first two items of this array represent Unicode code points for the first and the last letters in this font block. After those items, comes a list of offsets to each letter. Using those offsets, the code is able to find the letter data inside the font block array. The letter data consists of a four-byte header and a variable length bitmap. The four header bytes are: bitmap width, height, size in 32-bit words and glyph y-offset. The y-offset is needed because the bitmap doesn’t contain any blanking on top of the letter glyph. This way bitmaps consume less space and they are faster to draw. The bitmap data basically contains horizontally packed glyph pixels. The top left pixel is the 7th bit of the first byte, the pixel next to it on the right is the 6th bit and so on.
You may wonder why the font is divided into blocks. This is basically, because many fonts don’t contain all the Unicode letters. I used Arial Unicode MS font in this project. While it contains many Unicode ranges, some (uncommon) ranges are missing. It wouldn’t be wise to have one big array for letters from 0 to 0xFFFF, because there would be "holes" in it and the array would consume more space. That’s why the font is divided into six blocks. Each time the code needs to access some letter, it must iterate over those blocks in order to find the one which contains the letter. This, of course, introduces some overhead, but six blocks seems to be a good trade-off between the speed and the space consumption.
The font blocks are generated in two sizes (10 and 13) and two variants (regular and bold). The CJK block is only generated in size 13, though. All the font blocks together consume about 3.4 MB.
Now, let’s talk about how fonts are embedded in the application binary. If the fonts and the application code would consume together less than 1 MB of space, it would be possible to access the font data directly, in the same way as with any other normal array (with a pointer, for example). This is, however, not the case and that’s why we need to store font arrays in their own binary segment. The limitation of 1 MB comes from the ESP8266. It is only able to directly access 1 MB of the flash space. It is however possible to use spi_flash_read SDK function to read four bytes from any flash address.
The following allows us to store fonts separately from the application code:
- The font array is declared with __attribute__((section(".font.text")))
- The linked script contains the "font" named segment with proper base address and size
- esptool is modified to produce a binary file from the font segment data
That’s why in this project we have three binary segments: two for the application code and one for the fonts. The font segment needs to be flashed only once, if no changes are made to the fonts. After making changes to the application code, you only need to flash the application segments, which is much faster.
So, let’s wrap up the whole process from receiving a tweet to showing it on the display.
- The JSON data containing the tweet is received over HTTPS
- The tweet text, user name and like/retweet counter values are parsed out from the JSON
- Unicode-encoded characters are decoded and the tweet text is converted into a wide character string
- For each character in this string:
- Find the font block character belongs
- Find the character offset and jump to the character data
- Read character bitmap header
- Copy the bitmap to the display frame buffer according to the header values
- Increment the cursor by the width of the character
- Finally, the frame buffer is sent to the display
The process above is a little bit simplified, because additionally, the word wrapping, keywords highlighting and automatic font size selection are performed during the process.