2016年3月29日 星期二

在 STM32F469 上用 FreeType 讀取 TrueType 字型繪製文字


有炫砲的繪圖加速, 就會想要拿來做圖形界面
說到圖形界面, 第一個要解決的就是文字顯示問題了
所以本實驗室找來了 FreeType 作為文字解決方案
參考對岸文章:第22章 TrueType矢量字体



FreeType 是一套讀取字型的函式庫, 幾乎只要非 Windows 平台想要繪製文字都是找它
Android 也是用它來產生文字, 據說維護人員中曾有人是 Google 員工
它可以讀取點陣字型以及向量字型, 我們最有興趣的肯定是向量字型, 這裡有些說明:
An Introduction to TrueType Fonts: A look inside the TTF format
可以任意放大並維持漂亮的邊緣, 由於都是運算產生的, 所以運行平台需要有足夠的本事
速度要夠快, 記憶體也要夠多, 至於應該要多少則和繪製的文字大小有關
由於都是動態配置記憶體, 要使用它第一個要解的問題就是讓 malloc 和 free 這兩函數能用
在 linux 為基礎的平台沒有任何問題, 而在 stm32f4 系列平台就要動些手腳

不過在動手前, 我們得先看看怎麼使用 FreeType
上面的對岸文章有放一個連結 emwin_freetype.zip , 出自 emWin Fonts
從名稱看應該是 ST 官方找的合作夥伴, 在前篇 在 ubuntu 上開發 STM32F469 Discovery 實驗板
編譯官方 demo 時就有看到這套件的標頭檔和 lib, 不給源碼
而這供應商有提供方法來使用 FreeType, 並放出源碼供參考
由於是 GPL, 使用時若包進韌體, 就要同時公開韌體的源碼, 這協議具有感染性
如果是要商業用, 就要去談費用, 反正不管找誰都是要給錢, 就差在售價和功能

我把這包源碼整進我的開發環境, 這裡放出源碼供參考:ft-test.zip
為了避免無止盡的燒錄嘗試, 我先在桌上型電腦進行測試, 可參考這包裡的 ft-test/ft-test 目錄
直接 make all 即可編譯, 它沒有依賴別的 lib, 看來應該是被修改過了
桌上型電腦的 freetype 會依賴 zlib

執行結果應該會像這樣:


直接在 console 中印出繪製結果, 這隻測試程式參考 emWin_V5.26 中的 GUI_TTF.c
這是他們的轉換層, 把 FreeType 輸出轉成他們的函式庫可以用的文字圖案
每個像素一個 byte, 0-255, 0 代表空白, 255 代表文字最深顏色, 這是反鋸齒過的, 服務超棒!
不同於一般點陣字庫只有 0 和 1 的 1-bit 圖形
我這隻桌上型電腦的測試程式把大於 0xC0 的像素以 ▉ 表示
在 0xC0 到 0x81 的用 ▒ 表示, 0x80 到 0x41 的是  ░ , 剩下 0x40 以下的印空白

測試完成後確認能用, 我們就要把它搬到 stm32 上運行
第一個要解的問題就是讓 malloc 和 free 這兩函數能用
要讓 malloc 能動, 要讓軟體有空間可以搭建 heap
我承認在唸書時從來沒有去仔細研究這傢伙, 它很機車, 很難搞XD
不過在這裡我們也不用去搞懂它, 只要知道怎麼配置一個空間讓它丟東西進去即可
配置空間先從 LD 腳本開始, 在 stm32f469ni.ld 中

MEMORY {
    RAM      (RWX) : ORIGIN = 0x20000000,       LENGTH = 320K
    EXTSRAM  (RWX) : ORIGIN = 0xC0000000,       LENGTH = 6692K /* other for framebuffer */
    FLASH    (RX)  : ORIGIN = 0x08000000,       LENGTH = 2048K
    CCMRAM   (RW)  : ORIGIN = 0x10000000,       LENGTH = 64K
    SPIFLASH (RX)  : ORIGIN = 0x90000000,       LENGTH = 32M
}

stm32f4 會把 SD Ram 映射到 0xC0000000 上, 大小依選的記憶體而定
我這板子配的記憶體應該是 16MB, 但不知道為什麼我只能寫 8MB, 不知道是哪裡設定錯
這裡先不管, 8MB 已經很夠用了, 這問題以後再查
我把 0xC0689000 到 0xC0800000 這段空間拿去給 LCD 畫螢幕用, 可參考 src/main.h 中說明

/* ==========================================================================
                    M E M O R Y   C O N F I G U R A T I O N
   ========================================================================== */
#define LCD_LAYER0_FRAME_BUFFER  ((int)0xC0689000)
/* 800 x 480 x 16-bit = 768000 bytes = 0xBB800
   0xC0689000 + 0xBB800 = C0744800 */
#define LCD_LAYER1_FRAME_BUFFER  ((int)0xC0744800)
/* 800 x 480 x 16-bit = 768000 bytes = 0xBB800
   0xC0744800 + 0xBB800 = END of EXTSRAM : 0xC0800000 */

layer 0 是底圖, layer 1 則是疊在上面的半透明圖層, 目前我只使用 layer 0, layer 1 保留備用
因此 0xC0000000 到 0xC0689000 間就有約 6692KB 的空間可用
我們把這空間指定給 _heap_size (在 stm32f469ni.ld)

_heap_size      = LENGTH(EXTSRAM);              /* required amount of heap  */

然後 stm32f469ni.ld 後半段加上 symbol 定位

._user_heap_stack : {
    . = ALIGN(4);
    PROVIDE ( end = . );
    PROVIDE ( _end = . );
    PROVIDE ( __end__ = . );
    /* end of allocated ram _end */
    PROVIDE( _HEAP_START = _end );
    . = . + _heap_size;
    /* end of the heap -> align 8 byte */
    PROVIDE ( _HEAP_END = . );
    . = ALIGN(4);
} >EXTSRAM

接著參考德州儀器的說明:HowTo : get malloc to work
不過由於記憶體配置不一樣, 有做些修改, 可參考 src/startup.c 最下方新增的 _sbrk 函數

/* low level bulk memory allocator - used by malloc */
caddr_t _sbrk ( int increment ) {
    caddr_t prevHeap;
    caddr_t nextHeap;
    
    if (heap == NULL) {
        /* first allocation */
        heap = (caddr_t)&_HEAP_START;
    }

    prevHeap = heap;
    /* Always return data aligned on a 8 byte boundary */
    nextHeap = (caddr_t)(((unsigned int)(heap + increment) + 7) & ~7);        
    /* get current stack pointer */
    register caddr_t stackPtr asm ("sp");
    
    /* Check enough space and there is no collision with stack coming the other way
       if stack is above start of heap
    if ( (((caddr_t)&_HEAP_START < stackPtr) && (nextHeap > stackPtr)) || 
       WuKC: we put heap in EXTSRAM, no collision with stack so don't check stack */
    if(
         (nextHeap >= (caddr_t)&_HEAP_END)) {    
        return NULL; /* error - no more memory */
    } else {
        heap = nextHeap;
        return (caddr_t) prevHeap;    
    }    
}

裡面會引用在 stm32f469ni.ld 中定義的 _HEAP_START 和 _HEAP_END
原先德州儀器範例會檢查 stackPtr, 我們的 stackPtr 被設定在 stm32f4 內建 SRAM
stm32f469ni.ld:

_estack         = ORIGIN(RAM)+LENGTH(RAM);      /* end of the stack */

src/startup.c:

__attribute__ ((section(".isr_vectors")))
void (* const g_pfnVectors[])(void) = {
    (intfunc)((unsigned long)&_estack), /* The stack pointer after relocation */
    Reset_Handler,              /* Reset Handler */
...

它們不會衝突, 所以把判斷式刪減成只檢查 _HEAP_END
這個 _sbrk 是 malloc 需要的函數, 如果我們不需要 malloc, 就要把 makefile 中這行解除註解

# CFLAGS+= --specs=rdimon.specs -Wl,--start-group -lgcc -lc -lm -lrdimon -Wl,--end-group

這行是原先用來解找不到 _sbrk 函數問題的方案, 不過了解了它的用途
我想應該可以填個總是傳回 NULL 的 _sbrk, 這意思應該是一樣的, 有機會再試試

在進行這實驗時曾跳進 HardFault_Handler 中, 對岸有文章說明可能原因
STM32发生hardfault_Hander故障原因及处理方法整理
大概是記憶體爆炸或是 stack 爆炸, 為了檢查, 我修改了 src/startup.c
把 Default_Handler 改成用 define, 展開到預設中斷向量中

#define DEFAULT_HANDLER led_on(5, 1);while(1){}

這樣的結果是爆炸卡死時並不會跳到其他函數, 只要查閱當前 PC 就能知道卡死在哪個向量中
卡死後對 openocd 下命令

> halt                                      
stm32f4x.cpu: target state: halted
target halted due to debug-request, current mode: Handler HardFault
xPSR: 0x81070003 pc: 0x080003fe msp: 0x2004fe98
> reg                                       
===== arm v7m registers
(0) r0 (/32): 0x00000010
(1) r1 (/32): 0x00000001
(2) r2 (/32): 0x00000030
(3) r3 (/32): 0x40020C00
(4) r4 (/32): 0x00000007
(5) r5 (/32): 0x90000000
(6) r6 (/32): 0x20000480
(7) r7 (/32): 0x00000001
(8) r8 (/32): 0x00000002
(9) r9 (/32): 0x00000000
(10) r10 (/32): 0x00000000
(11) r11 (/32): 0x00000000
(12) r12 (/32): 0x2000006C
(13) sp (/32): 0x2004FE98
(14) lr (/32): 0x080003FF
(15) pc (/32): 0x080003FE
(16) xPSR (/32): 0x81070003
(17) msp (/32): 0x2004FE98
(18) psp (/32): 0x00000000
(19) primask (/1): 0x00
(20) basepri (/8): 0x50
(21) faultmask (/1): 0x00
(22) control (/2): 0x00
...

可以看到 PC 在 0x080003FE, 這是內部 Flash 映射的空間
用 objdump 反組譯 test.elf 就可以看到它在哪個函數裡

如果想查是從哪裡引發這個中斷, 根據 ARM 說明

在跳進 interrupt 時, stack 指標會以一些固定規則把之前運行中的函數的暫存器 push 進 stack
如果把 stack 指標附近的記憶體 dump 出來應該可以找到, 看上面這行

(13) sp (/32): 0x2004FE98

接著 dump, 對 openocd 下

> mdw 0x2004FE70 32
0x2004fe70: 2000c960 20008878 08040168 0801986f 0800d7a1 0801b1fd 00000007 90000000
0x2004fe90: 20000480 080003ff 200008a8 fffffff9 20000480 90000000 00005791 200008a8
0x2004feb0: 2000006c 0803e2e5 0803e2e4 a1070000 00000007 00000002 20000958 00000001
0x2004fed0: 00000002 0800138d 0800114d 00000000 2004feec 00000001 20008890 2000d5a0

不過我並沒有看到如同 ARM 說明的記憶體往高處找應該要找到前一函數
反而是往低處找時找到可疑項目, 不知道是硬體有修改, 還是我誤解文章說明
比對反組譯出來的結果:

0800d7a0 <af_autofitter_done>:
 800d7a0:    b538          push    {r3, r4, r5, lr}
 800d7a2:    4604          mov    r4, r0
 800d7a4:    2500          movs    r5, #0
...

08019866 <ft_mem_free>:
 8019866:    b508          push    {r3, lr}
 8019868:    b109          cbz    r1, 801986e <ft_mem_free+0x8>
 801986a:    6883          ldr    r3, [r0, #8]
 801986c:    4798          blx    r3
 801986e:    bd08          pop    {r3, pc}

08040168 <autofit_module_class>:
 8040168:    00000004     andeq    r0, r0, r4
 804016c:    000000c8     andeq    r0, r0, r8, asr #1
...

就可以知道在誰附近爆炸了, 以我的例子是 free 時出錯
後來查到是漏把桌上型電腦測試程式的 free 刪除, 它 free 成我的 QSPI 映射空間, 難怪會炸
刪掉後, 只要是 malloc 做的都能 free, 沒有問題

執行正常後, 接著就是把它弄到 LCD 上了
文字送進 FreeType 前要先轉出字碼, UTF-8 字串有一定的格式編碼
可參考 src/ftfont.c 中的函數 ftfont_get_utf8_char
照著 wiki 上的介紹就可以寫出, 很容易, 解出一個 32-bit 整數
把這整數送給 FreeType, 就可以要到一塊畫有文字的 buffer
如果直接用 2D 繪圖加速把這塊文字 buffer 丟上去, 它會變這樣:


一片慘綠XD 這 buffer 是個 8-bit 顏色, 但是 RGB 不知道怎麼排
我只聽過 888, 666, 565, 1555, 444, 要擠進 8-bit 只有 DOS 時代那種配色
而我把 8-bit 顏色全部畫出來, 也就是上圖下半像條碼的, 左邊 0 右邊 255
也看不出啥規則, 查一下 datasheet, 這個顏色格式是 L8, 沒聽過
後來再多看幾頁才大概了解, 應該是 indexed color, 也就是 0-255 只是 index
它會指向顏色表, 顏色表可以設定到記憶體, 也可以在暫存器中
能放暫存器的當然就放囉, 記憶體省著點用, 在 src/main.c 中這裡

for(i=0; i<256; i++){
    U32 color = 0xFF000000; // ARGB
    U8 c = (0xFF - i) & 0xFF;
       
    color += c | (c << 8) | (c << 16);
    DMA2D->FGCLUT[i] = color;
}
DMA2D->FGCMAR = (U32)DMA2D->FGCLUT;

預設是 256 組 32-bit color, 我把它填為 256 色灰階
然後再畫一次上面的圖就變成這樣


讚啊!
由於是 indexed color, 遇到不同前景和背景色的組合時, 會需要自行線性內插
當前這範例是黑底白字, 所以填 256 色灰階

不過如果仔細看, 其實文字排列歪歪的, 如果再多塞點不同語言的文字就會發現問題


用 Google 翻譯的, 內容請無視XD
注意全形的逗號, 如果依照 FreeType 返回的 buffer, 直接照尺寸畫上
它就只有逗號有形狀的區域, 也就是 minimal bounding box 的區域
看來在 FreeType 返回的 FTC_SBitRec 結構中有更多重要的訊息
FreeType 的程式很大, 那是已經有十年以上歷史的軟體, 短時間內不可能看完
我沒有花時間把細節挖出, 用試的找出一些規則, 重新擺放
也就是取得 buffer 後不要從固定座標去畫, 而是加減一些 FTC_SBitRec 結構中的數據
這段程式寫在 src/main.c 的 ftfont_test 函數中
最後得到這結果:


逗號已經是全形的了, 但是繪圖時仍然只畫有逗號形狀的區域, 只是繪製座標被修改了
如果是空格字元, 傳回的圖形 buffer 會是 NULL, 沒有東西, 必須依賴其他數據來決定空格大小
這四種語言我有概念, 它們的排列位置應該是正確的
後來無聊試了一下阿拉伯文, 發現都是方框, 那個我就沒辦法了XD

完成後為了方便日後開發要稍微打包一下
首先先設定放字型的位置, 我們有高達 16MB 的 QSPI Flash, 不用白不用
可參考 src/res.c

__attribute__ ((section(".font_buffer"))) static unsigned char font_buf[FT_FONTSIZE];

我們宣告了一個巨大的陣列, 它的體積是 FT_FONTSIZE, FT_FONTSIZE 在 ft.mk 中以 du 產生

FT_FONTSIZE_STR=$(shell du -b DroidSansFallbackFull.ttf)
FT_FONTSIZE=$(firstword $(FT_FONTSIZE_STR))

用 du 去列出 DroidSansFallbackFull.ttf 這字型檔的體積, 然後取第一個數字
接著用編譯器參數 -DFT_FONTSIZE=$(FT_FONTSIZE) 定義
它就會自動引入原始碼中, 換檔案後不用手動更新
這個巨大陣列先放在 .font_buffer 這個 section 中
然後在 stm32f469ni.ld 中設定:

.ExtQSPIFlashSection : {
     KEEP(*(.font_buffer))
     *(.ExtQSPIFlashSection)
} >SPIFLASH

讓它永遠排在 QSPI Flash 一開始的地方, 這樣我們就可以在 ft.mk 中直接用 cp 來複製字型

mkfont_qspi: all
    dd if=$(APP_NAME)-spi.bin of=$(APP_NAME)-spi.tmp bs=1 skip=$(FT_FONTSIZE)
    cp -a $(FT_FONTFILE) $(APP_NAME)-spi.bin
    cat $(APP_NAME)-spi.tmp >> $(APP_NAME)-spi.bin
    rm -f $(APP_NAME)-spi.tmp

先 dd 字型以外的資料到暫存檔, 接著把字型複製過來, 再把暫存檔 cat 到字型後面
如果我們把字型像官方 demo 範例那樣都做成標頭檔, 檔案量會很驚人, 而且更換不易
所以才改這樣稍微複雜一點的方案

要測這個 FreeType 測試, 和官方 demo 一樣要燒兩次
先 make mkfont_qspi 把韌體和 QSPI Flash 做出來
如果只有 make all 並不會包入字型, 因為那資料是固定不變的
只要在需更新 QSPI Flash 時再做出來即可, 平常編譯測試時都放垃圾進去沒差
完成後依照前篇 在 ubuntu 上開發 STM32F469 Discovery 實驗板 的流程
把 test-spi.bin 放到 SD 卡燒上去, 然後再燒 test.bin

這次我們專注在怎麼把 FreeType 丟上去跑, 顯示部份只有概略說明
顯示和 2D 繪圖加速部份的程式寫在 src/lcd.c , 這是從官方 demo 抽出後修改來的
從這程式內容看來, STM32F4 系列的繪圖加速包含圖層混合, 半透明混合, 區塊複製, 顏色轉換
似乎沒有看到像素對像素的運算, 像是兩張圖每個點做計算後放到另一張圖上

出自:directfb png透明色问题
這是桌上型電腦或是手機平板方案才會有的硬體加速
雖然對 MCU 來說有 2D 繪圖加速已經是很神奇的事了
但有一就有二, 做出來了, 之後要求就會越來越高XD
我在唸書的時候花了很多時間了解 UI 底層設計以及 2D 繪圖加速
那是非常 "厚重" 的技術, 很多細節, 即使花了數個月得到的知識也只能算皮毛
在 UI 軟體中搞定文字之後就是要搞繪圖函式庫, 畫多邊形, 曲線, 並填色以及反鋸齒
再往底層就是繪製過程中如何引入繪圖加速, 畫完之後再用繪圖加速扔到螢幕上
以前書念的不夠, 只能摸出輪廓, 但對於實做細節知道的很少
STM32F4 雖然沒有像現在手機平板那種等級的硬體加速, 但是仍然是非常好的入門平台
先玩簡單的, 才有辦法往上爬, 如果有玩出一些心得再另外貼

4 則留言:

  1. 请问我按照您的方法,有些文字是□,感觉是编码不对。

    回覆刪除
    回覆
    1. 開發環境是 utf-8 的 C 檔嗎? 若程式有讀取外部文字檔, 文字檔是 utf-8 嗎?
      還有字型檔裡對應的字碼是有資料的嗎? 我想能查的大概就這些地方
      若是 BIG5/GB 等編碼要先轉去 utf-8, 或是去讀用該編碼的字型

      刪除
  2. 谢谢您的回复,的确是我utf8转的问题。另外请问如何得到行高?例如汉字一,他的高度是小于top(基线到上部距离),如果一行文字都是一,那样行高就不对了。freetype有什么办法能得到字体的行高呢?就像word一样,改变字体的大小,他直接就会得到行高。

    回覆刪除
    回覆
    1. 這個問題我不知道正確解答, 而上網找了一下...
      https://stackoverflow.com/questions/28009564/new-line-pixel-distance-in-freetype
      別人也說沒有保證行高, 有可能遇到一些奇怪字元就會超過
      而該問題被選擇的答案是:把整個字型檔所有字碼掃過一遍, 然後算出安全值
      畢竟老美的字碼不多, 他們應該大多不知道 CJK 字型是吃掉字型檔案空間的元兇XD

      以我使用 ubuntu 上的 python gtk 的文字框來看, 各個版本的處理方式有些不同
      約 ubuntu 15 以前中文會被裁切一部分
      約 15~18 之間則是當輸入英文時文字框維持預設高度, 而輸入中文字後自動拉高行高
      18 之後也就是現在的則是預設行高就比多數中文字還高
      ubuntu 下的文書軟體 LibreOffice Writer 目前版本則是輸入中文後才拉高
      但刪除中文字後會退縮回英文行高, 大概有這幾種解法
      我覺得全掃一遍不合理, 中文字數量太多了, 若我來做大概就預設行高或動態增加二選一
      預設行高可以找幾個尺寸比較極端的字做基準, 像是什麼四條龍 (龘) 這類四疊字來算
      這樣只要問幾次行高就可以得到安全值, 至少大多數情況我們不會去用四疊字, 這高度一定安全XD

      刪除

注意:只有此網誌的成員可以留言。