最近買了一個LCD12864的屏幕,驅動采用的是UC1701,SPI接口,字庫是高通的GT20L16S1Y。這里吐槽一下我拿到的官方字庫手冊,居然沒有指明各種樣式字體的起始地址。不過在廠家給的例程里倒是能估計出來。在使用字庫例程修改加上我自己的SPI驅動以后,便可以簡單的顯示漢字了。一開始以為就這樣就好了,然后我打算將之前寫的一個電子鐘模塊移植過來時,卻發現年,月,日這樣的字符都無法正常顯示:年是亂碼,月的字符偏移到了字庫里面“(六)”’該字符的位置,日的字符則是直接顯示“查”字。我一開始覺得是驅動有問題,但是仔細想想又說不通,畢竟ascii字符也是可以讀取出來的。使用邏輯分析儀查看除了返回字模錯亂,其他也是一切正常。如果驅動有問題,也沒可能那么準確的返回另一個字的字模。然后我就考慮是不是其他地方在copy的時候出現了錯誤,類型不對溢出等。因為之前吃過這個虧,移植了一個stm32的程序到51上,結果MDK開發包與c51開發包的unsigned int類型有差異,導致了程序無緣無故的錯誤。
萬萬沒想到這次又吃了這個虧。
因為尋址算法是官方例程提供的,我也沒有多考慮,debug的時候直接排除了這部分。并且在網上查找對比發現這套GB2312的計算方法確實是可行的。我把精力一直放在字庫偏移的問題上,進行測試發現,GB2312庫使用我的程序,大約在“繭”這個字左右開始,之后的所有字,要么是亂碼的無法顯示,要么是直接偏移到了前面的區位段去。雖然出錯,但是讀出的又是完整的字。很明顯發生了類似的溢出行為,但我百思不得其解,我代碼段里變量都已經修改為unsigned long了,怎么還能溢出呢?該段代碼如下:
/***************************************************
16 點GB2312 標準點陣字庫
參數說明:
GBCode表示漢字內碼。
MSB 表示漢字內碼GBCode 的高8bits。
LSB 表示漢字內碼GBCode 的低8bits。
Address 表示漢字或ASCII字符點陣在芯片中的字節地址。
BaseAdd:說明點陣數據在字庫芯片中的起始地址。
r_dat_bat 是讀點陣數據函數。
DZ_Data是保存讀出的點陣數據的數組。
*****************************************************/
void gt_16_GetData (u8 MSB,u8 LSB)
{
u32 BaseAdd=0,Address;
if(MSB == 0xA9 && LSB >=0xA1)
Address = (282 + (LSB - 0xA1))*32+BaseAdd;
else if(MSB >=0xA1 && MSB <= 0xA3 && LSB >=0xA1)
Address =( (MSB - 0xA1) * 94 + (LSB - 0xA1))*32+ BaseAdd;
else if(MSB >=0xB0 && MSB <= 0xF7 && LSB >=0xA1)
Address = ((MSB - 0xB0) * 94 + (LSB - 0xA1)+ 846)*32+ BaseAdd;
r_dat_bat(Address,32,DZ_Data);
}
在某一次搜索中,發現了中景園的字庫例程,我簡單的對比了一下,基本上是相同的,但是在燒寫程序以后,中景園給的例程完全正常,除了臭名昭著的0xfd bug(具體可以在論壇或者網絡上搜索),顯示完全沒問題。看了一眼程序注釋,恍然大悟。代碼如下(篇幅問題只放一部分作對比,具體可站內搜索):if(((text>=0xb0) &&(text<=0xf7))&&(text[i+1]>=0xa1))
{
/*國標簡體(GB2312)漢字在晶聯訊字庫IC中的地址由以下公式來計算:*/
/*Address = ((MSB - 0xB0) * 94 + (LSB - 0xA1)+ 846)*32+ BaseAdd;BaseAdd=0*/
/*由于擔心8位單片機有乘法溢出問題,所以分三部取地址*/
fontaddr = (text- 0xb0)*94;
fontaddr += (text[i+1]-0xa1)+846;
fontaddr = (ulong)(fontaddr*32);
addrHigh = (fontaddr&0xff0000)>>16; /*地址的高8位,共24位*/
addrMid = (fontaddr&0xff00)>>8; /*地址的中8位,共24位*/
addrLow = fontaddr&0xff; /*地址的低8位,共24位*/
get_n_bytes_data_from_ROM(addrHigh,addrMid,addrLow,fontbuf,32 );/*取32個字節的數據,存到"fontbuf[32]"*/
display_graphic_16x16(y,x,fontbuf);/*顯示漢字到LCD上,y為頁地址,x為列地址,fontbuf[]為數據*/
i+=2;
x+=16;
}
這里提了一個關鍵詞 乘法溢出。之前在C語言,C++里面,相信大家都學習過不少相關的知識。例如整型提升,強制類型轉換等。uchar溢出的問題在剛寫程序的時候也經常會遇到,常常是定時器相關。這里引用一名為的“keil大常量計算問題”的博客來做解釋,我自己講的可能不太準確。
該博客提到:
keil C51是與ANSI C兼容的編譯器,ANSI C規范規定十進制整數常量的默認數據類型是int、long int和unsigned long int的其中一種,對給定的常量是其中的哪一種要看這個常量的實際大小,如果常數在-32768~32767之間則按int類型處理,如果按int類型處理會溢出就考慮long int或更大的數據類型unsigned long int。總之,編譯器總是按盡可能的原則指定常量的類型。但這一原則并不總能奏效,當兩個常量做運算時就可能導致溢出。如:
#define SYSCLK 22118400// SYSCLK in Hz (22.1184 MHz external crystal oscillator)
#define SLIDER_REST_TIME 100// in ms,slider rest time
#define REST_DELAY SYSCLK * SLIDER_REST_TIME / (65536 * 1000)
unsigned char i;
i = REST_DELAY;
在keil c51中運行i為0xE1,即225,并不是期望的結果22118400 * 100 / (65536 * 1000) = 33.75,取整為33。原因分析如下:
宏替換后為:i = 22118400 * 100 / (65536 * 1000);,編譯器首先為22118400定義類型,因為22118400不在int的表示范圍內,而在long int的范圍-2147483648~2147483647內,所以22118400按long int類型處理,在做乘積運算時100被自動按long int處理,22118400 * 100將按兩帶符號長整型常量進行運算,運算結果仍為帶符號長整型,結果寫成十六進制是0x83D60000,其十進制是-2083127296,顯然出現了溢出錯誤。keil編譯器并沒有給出任何錯誤或警告提示信息(VC++6.0還給出警告warning C4307: * : integral constant overflow),繼續進行下一個運算65536 * 1000,結果為帶符號長整型,十六進制為0x3E80000,十進制為65536000,最后按兩長整型除法計算-2083127296 / 65536000,結果為0xFFFFFFE1,由于i為字符類型,取0xFFFFFFE1的最低有效字節為0xE1賦值給i,i的最終值為0xE1。
解決這種溢出錯誤的方法用C語言的一個術語就是“提升”(promotion),拿上例來說就是將22118400指定為無符號長整型,即:
# define SYSCLK 22118400UL
注:雖然只要將22118400、100、65536和1000四個常數中的一個指定為無符號長整型即可得到正確的結果,但考慮到可讀性及規范性,應選擇大整數指定其類型。-----------------------------
通過以上說明我們可以看見,這一切起因是常量造成的整型提升。以我的字庫程序為例就是94,被默認成為了int,而繭對應的機內碼為BCEB,MSB為BC,LSB為EB,經過計算可以得到,該式子為(1128+74+846)*32,合并一下,即為2048*32,這個數字大家一定很熟悉吧,2的十五次,對應int類型,恰好發生溢出。因此,繭字以后發生溢出便得到了解釋。本身LSB和MSB屬于無符號char,但常數94等默認為有符號int,在一起計算的過程中uchar被提升,整體結果以int的形式呈現,而這時中間結果發生了溢出,無法在正常通過賦值提升為ulong類型提供正確地址。因此,字庫顯示范圍永遠被現在那個大小。再看中景園的程序,實際上是提前進行ulong轉化的步驟,提前存儲中間結果,防止其在進行乘法時溢出。同時也可以想到常量后綴的方法,將94等修改為94L,或者使用(unsigned long)進行強制轉化,則程序也可以正常運行。
正常運行.png (15.73 KB, 下載次數: 77)
下載附件
正常運行
2020-7-8 22:45 上傳
8位單片機存在乘法溢出.png (13.25 KB, 下載次數: 80)
下載附件
存在乘法溢出風險
2020-7-8 23:03 上傳
因此,大家以后在做這種容易涉及溢出的問題的時候,一定要多留個心眼,例如及時將常量加上后綴,以防止意料之外的溢出問題產生。我這個源程序其實是沒問題的,MDK環境下能正常運行,因為int的位數問題,導致了stm32使用該程序時,字庫無論怎么取也不會出現這個溢出。因此大家在移植程序時也要在這方面多加考慮。
總體看其實是一個相當愚蠢的問題,卻困擾了我好多天。。一直沒想到乘法溢出的問題,C語言功底還是不夠扎實。總之吃一塹長一智,希望大家能吸取我的經驗教訓吧,也希望能幫到使用了該例程而感到困惑的朋友。
|