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