看到人家的作品, 最好事現買現用,
(http://www.obdev.at/products/avrusb/easylogger.html)
可惜原本的 ATTINY45 有 4K 空間, 足夠寫入 2K多一些的程序. 手邊只有 ATTINY26, 只有 2K 空間, 無法容納這個程序, 不管如何減肥, 還是超出大概 4% 的容量. 如何解決?!
實驗的結果, 只要把其中以下的一段刪除, 容量馬上降到 96%, 證明可以有機會減肥成功. 但是完全看不懂下面的內容和寫法, 所以只好列表看看到底在做些什麼...
-----------------原來 C 語言程序
static void evaluateADC(unsigned int value)
{
uchar digit;
value += value + (value >> 1); // value = value * 2.5 for output in mV
nextDigit = &valueBuffer[sizeof(valueBuffer)];
*--nextDigit = 0xff;// terminate with 0xff
*--nextDigit = 0;
*--nextDigit = KEY_RETURN;
do{
digit = value % 10;
value /= 10;
*--nextDigit = 0;
if(digit == 0){
*--nextDigit = KEY_0;
}else{
*--nextDigit = KEY_1 - 1 + digit;
}
}while(value != 0);
}
-----------------原來 C 語言程序
看起來, 這個由 AVRStudio 產生的列表, 並沒有預期的容易看得懂, 比對一下,
---------------翻譯後的 C 語言程序
evaluateADC(ADC);
214: 84 b1 in r24, 0x04 ; 4
216: 95 b1 in r25, 0x05 ; 5
static void evaluateADC(unsigned int value)
{
uchar digit; 暫時的變數
value += value + (value >> 1); // value = value * 2.5 for output in mV
218: 28 2f mov r18, r24 ; ADCL 的數值轉存到 r18
21a: 39 2f mov r19, r25 ; ADCH 的數值轉存到 r19
21c: 36 95 lsr r19 ; ADCH bit 0 移到 C bit
21e: 27 95 ror r18 ; C bit 移到 ADCL bit 7, bit 0 移到 C bit
220: 88 0f add r24, r24 ; ADCL + ADCL
222: 99 1f adc r25, r25 ; 如果有進位, 加入到 ADCH
224: 28 0f add r18, r24 ;
226: 39 1f adc r19, r25
nextDigit = &valueBuffer[sizeof(valueBuffer)];
*--nextDigit = 0xff;// terminate with 0xff
228: f0 92 7a 00 sts 0x007A, r15 ; 存入指針 (* nextDigit)的位置, 並且 指針 - 1
*--nextDigit = 0;
22c: 10 92 79 00 sts 0x0079, r1 ; 存入指針 (* nextDigit)的位置, 並且 指針 - 1
*--nextDigit = KEY_RETURN;
230: 00 93 78 00 sts 0x0078, r16 ; 存入指針 (* nextDigit)的位置, 並且 指針 - 1
234: ec 2f mov r30, r28 ; YL -> ZL
236: fd 2f mov r31, r29 ; YH -> ZH
238: 32 97 sbiw r30, 0x02 ; Z - 2
do{
digit = value % 10 ; % = mod 找餘數, value 被除數 dividend, 10 除數 divisor, digit 餘數 remainder
23a: 82 2f mov r24, r18 ; ADCL
23c: 93 2f mov r25, r19 ; ADCH
23e: 6a e0 ldi r22, 0x0A ; 10
240: 70 e0 ldi r23, 0x00 ; 0
242: e6 d2 rcall .+1484 ; 0x810 <__udivmodhi4>
244: 48 2f mov r20, r24
value /= 10;
246: 82 2f mov r24, r18
248: 93 2f mov r25, r19
24a: 6a e0 ldi r22, 0x0A ; 10
24c: 70 e0 ldi r23, 0x00 ; 0
24e: e0 d2 rcall .+1472 ; 0x810 <__udivmodhi4>
250: 86 2f mov r24, r22
252: 97 2f mov r25, r23
254: 28 2f mov r18, r24
256: 39 2f mov r19, r25
*--nextDigit = 0;
258: 11 82 std Z+1, r1 ; 0x01
if(digit == 0){
25a: 44 23 and r20, r20
25c: 21 f4 brne .+8 ; 0x266
25e: 6e 2f mov r22, r30
260: 5f 2f mov r21, r31
*--nextDigit = KEY_0;
262: 10 83 st Z, r17
264: 04 c0 rjmp .+8 ; 0x26e
266: 6e 2f mov r22, r30
268: 5f 2f mov r21, r31
}else{
*--nextDigit = KEY_1 - 1 + digit;
26a: 43 5e subi r20, 0xE3 ; 227
26c: 40 83 st Z, r20
26e: 32 97 sbiw r30, 0x02 ; 2
}
}while(value != 0);
270: 89 2b or r24, r25
272: 19 f7 brne .-58 ; 0x23a
274: 72 cf rjmp .-284 ; 0x15a
---------------翻譯後的 C 語言程序
把這個 C 語言所寫的程序, 抽出來一部份, 再對照列表, 看看她到底在作什麼, 開始也看不懂, 後來明白了. 這個其實是一種 [除以10] 的算法, 目的是把一個數的每個位的數字單獨出來作為顯示用, 存在當然有他的理由. 看看下面,
(初時不明白, 後來看到因為 ADC differential input 有問題, 再去看看 forum, 原作者有回覆, 原理是一樣的解釋http://forums.obdev.at/viewtopic.php?t=1650&sid=6f952edc4acb56123f124fce1c592dd7)
-----------------原來 C 語言程序
do{
digit = value % 10; // 找出餘數
value /= 10; //找出商數
*--nextDigit = 0; // ??
if(digit == 0){ //如果餘數是0
*--nextDigit = KEY_0; // ??
}else{ //如果餘數不是0
*--nextDigit = KEY_1 - 1 + digit; // ??
}
}while(value != 0); //重複以上的步驟, 直到被除數為0才結束
-----------------原來 C 語言程序
舉例, 假如有一個整數 2130, 我們的直接反應, 看到 2, 1, 3, 0 四個阿拉伯數字, 腦袋裡面, 已經馬上運用學過的數學/算術, 讓普通人都理解為 2仟1百3十, 但是很少人會真正的研究到底我們的腦袋是怎麼樣得出來這樣的理解, 至少俺以前沒有想過, 因為我們的腦袋已經從小被訓練到有這個 [自動的機制], 不用詳細說明, 每個正常人都有的能力.
但是, 笨笨的電腦, 純粹是一部機器, 連這樣的能力都沒有的, 除非他也被訓練成有這樣的運算或理解能力. 所以以後不要以為 [電腦很神], 什麼都能.... [電腦減肥?!!!!!!!]
再來就要親自 [教導] 電腦執行這樣的運算, 用紙筆就可以推導這樣的演算法 (algorithm).
首先, 確定以 2130 / 10, 求得商數是213, 餘數是 0, 注意看的是餘數的部份,
Dividend 被除數 |
Divisor 除數 |
Quotient 商數 |
Remainder 餘數 |
|
第一步 | 2130 | 10 | 213 | 0 |
重複以上的動作, 不過這次是上一步的商數除以10, 確定以 213 / 10, 求得商數是21, 餘數是 3, 注意看的是餘數的部份,
Dividend 被除數 |
Divisor 除數 |
Quotient 商數 |
Remainder 餘數 |
|
第一步 | 2130 | 10 | 213 | 0 |
第二步 | 213 | 10 | 21 | 3 |
重複以上的動作, 不過這次是上一步的商數除以10, 確定以 21 / 10, 求得商數是2, 餘數是 1, 注意看的是餘數的部份,
Dividend 被除數 |
Divisor 除數 |
Quotient 商數 |
Remainder 餘數 |
|
第一步 | 2130 | 10 | 213 | 0 |
第二步 | 213 | 10 | 21 | 3 |
第三步 | 21 | 10 | 2 | 1 |
重複以上的動作, 不過這次是上一步的商數除以10, 確定以 2 / 10, 求得商數是0, 餘數是 2, 注意看的是餘數的部份,
Dividend 被除數 |
Divisor 除數 |
Quotient 商數 |
Remainder 餘數 |
|
第一步 | 2130 | 10 | 213 | 0 |
第二步 | 213 | 10 | 21 | 3 |
第三步 | 21 | 10 | 2 | 1 |
第四步 | 2 | 10 | 0 | 2 |
重複以上的動作, 不過這次是上一步的商數除以10, 確定以 0 / 10, 求得商數是0, 餘數是 0, 這樣就可以停止了.
Dividend 被除數 |
Divisor 除數 |
Quotient 商數 |
Remainder 餘數 |
|
第一步 | 2130 | 10 | 213 | 0 |
第二步 | 213 | 10 | 21 | 3 |
第三步 | 21 | 10 | 2 | 1 |
第四步 | 2 | 10 | 0 | 2 |
第五步 | 0 | 10 | 0 | 0 |
再次檢驗這個一步一步求得的表格, 注意看餘數那一列紅色的部份 (除了最底下的那個 0以外), 注意到了有沒有什麼特別 ?! 沒錯, 由底往上看, 讀出來就是 2, 1, 3, 0.
完全正確的把 2130 的這個數的每一個位的數字獨立出來. 這就是所謂 [除以10] 的算法, 目的是 [教導] 笨笨的電腦看到 2130 這個數的時候, 怎樣模仿人類的腦袋, 逐位讀出這些數字.
原作者可能故意, 或者是匆忙地做出這樣地程序, 當然沒有考慮優化或速度. 通常, 一般人做事, 會套用利民兄的一句名言 - 人工就收咁多, 重點是有多少時間做多少事, 達到目標後再看有沒有閒餘才把它完美. 至於俺為什麼要沒事找事做, 明明買顆同樣 4K 的 ATTINY45 就可以試了, 偏偏跑去學人家搞什麼塞到 2K 空間之類的事.......沒錯, 俺太閑, 除了吃喝拉撒睡等等耗費自然資源的指定動作外, 故意沒事找事做?!
很久以前, 馬文芳曾經說過, 一旦用到除法, 所有 MCU / CPU 都要耗很多時間或容量去執行計算, 除了特定設計過的程序或機器. 像老媽教訓兒子, 不要隨便男女共處一室, 否則兒子必有損失. 聽了就算, 有多少人真正能夠理解!? 直到........同樣, 馬文芳教俺的, 真正能夠理解的時候, 也是到了良久以後的事了.
所以, 這次抱著玩玩的心態, 趁機會也學學 C 語言, 看看人家的作品, 修修改改, 有了既定的目標, 又有一點基礎而不是由零開始, 進步神速. 確認一下減肥的目的和方向,
1) 盡可能不要用除法的運算, 減少耗時和記憶體空間;
2) 理解原作者的用意和程序的實際運作;
3) 想其他方案替代原來的程序, 看看空間是否需求變小;
為了簡化工作, 從原來的列表抽出一段, 目的是看看原來設計需要使用的空間大小,
-----------------原來 C 語言程序
do{
digit = value % 10; // 找出餘數
value /= 10; //找出商數
*--nextDigit = 0; // ??
if(digit == 0){ //如果餘數是0
*--nextDigit = KEY_0; // ??
}else{ //如果餘數不是0
*--nextDigit = KEY_1 - 1 + digit; // ??
}
}while(value != 0); //重複以上的步驟, 直到被除數為0才結束//
-----------------原來 C 語言程序
---------------翻譯後的 C 語言程序
do{
digit = value % 10 ; % = mod 找餘數, value 被除數 dividend, 10 除數 divisor, digit 餘數 remainder
23a: 82 2f mov r24, r18 ; ADCL
.....
.....
}while(value != 0);
270: 89 2b or r24, r25
272: 19 f7 brne .-58 ; 0x23a
274: 72 cf rjmp .-284 ; 0x15a
---------------翻譯後的 C 語言程序
這段 C 語言程序, 翻譯成機器碼後, 在列表看到的第一行, 最前面是 23a, 最後一行是 274, 大概計算一下,
0x274 - 0x23a = 0x3a, 把這個十六進制數目換算成人類喜歡看的十進制數目,
0x3a = 58, 意思就是說, 這段 C 語言程序, 為了把一個數, 例如 2310 轉換成 2, 3, 1, 0, 必須耗用 59 BYTE 的空間做為算法的處理.
其實並不止 59 BYTE, 因為真正在作除法的程序, 不管是求商數還是餘數, 例如,
242: e6 d2 rcall .+1484 ; 0x810 <__udivmodhi4>
......
......
24e: e0 d2 rcall .+1472 ; 0x810 <__udivmodhi4>
她是儲存在另外一處 (__udivmodhi4), 佔用的空間就不去計算了. 所以, 在改寫這個部份之前, 總共需要的空間是 2K 的 103.3%, 完全去掉的話, 總共需要的空間是 2K 的 96%. 這樣意味著, 佔用空間的改善結果起碼有 4%, 最重要的是, 同樣的功能, 2K 空間就可以塞得下, 效果實在顯著.
--------------------------------__udivmodhi4
00000810 <__udivmodhi4>:
810: aa 1b sub r26, r26
812: bb 1b sub r27, r27
.......
.......
836: 8a 2f mov r24, r26
838: 9b 2f mov r25, r27
83a: 08 95 ret
--------------------------------__udivmodhi4
Sep/01/2008, 繼續寫完這個還沒有完整的經驗.........
既然有了大概的想法, 真的要試試看這樣的構想能不能實現. 怎樣作?! 應用查表的方法, 就可以避開比較耗費資源的 [除以10] 演算法, 不過要有一點點折衷和妥協......
---------------翻譯後的 C 語言程序
evaluateADC(ADCH); //只要 8bit 精度
1fe: a5 b1 in r26, 0x05 ; ADCH = 0xML, M 或 L 可以是 0 到 F, 等於十進制 0 到 15
{
nextDigit = &valueBuffer[sizeof(valueBuffer)]; // point to address of the end of the buffer
*--nextDigit = 0xff;// terminate with 0xff, pointer -1, valuebuffer = 0xff
200: 00 93 8a 00 sts 0x008A, r16
*--nextDigit = 0; // valuebuffer = 0x00, 0xff
204: 10 92 89 00 sts 0x0089, r1
*--nextDigit = KEY_RETURN; // KeyID of RETURN or ENTER = 40, valuebuffer = 0x40, 0x00, 0xff
208: 10 93 88 00 sts 0x0088, r17
*--nextDigit = USBKeyID_table[value & 0x0f]; // Low 4bit of value, HEX_2_ASC or KEY PRESS add to buffer, valuebuffer = Low_4bit, 0x40, 0x00, 0xff
20c: ea 2f mov r30, r26 ; ADCH 存到 r30
20e: f0 e0 ldi r31, 0x00 ; r31 = 0
210: ef 70 andi r30, 0x0F ; r30 = ADCH 低4位數值 L (0到F)
212: f0 70 andi r31, 0x00 ; 明顯多餘的, 因為上面倒數第二行已經確定 r31=0
214: e0 5a subi r30, 0xA0 ; r31-r30, 16bit, 計算指針指向 USBKeyID_table 和 L 對應的數字
216: ff 4f sbci r31, 0xFF ; r31-r30, 16bit, 計算指針指向 USBKeyID_table 和 L 對應的數字
218: 80 81 ld r24, Z ; Z = r31-r30, 16bit 指針, 讀取USBKeyID_table的內的按鍵號碼, 存入 r24
21a: 80 93 87 00 sts 0x0087, r24 ; 按鍵號碼存入按鍵緩衝區, USB會[自動]發送, 等同人手按鍵
*--nextDigit = USBKeyID_table[(value>>4) & 0x0f]; // High 4bit of value, HEX_2_ASC or KEY PRESS add to buffer, valuebuffer = high_4bit, Low_4bit, 0x40, 0x00, 0xff
21e: d0 93 8c 00 sts 0x008C, r29 ; ??
222: c0 93 8b 00 sts 0x008B, r28 ; ??
226: a2 95 swap r26 ; 對調ADCH的高低4位, 例如, 0x12 變成 0x21
228: af 70 andi r26, 0x0F ; r26 = ADCH 高低4位數值 M (0到F)
22a: b0 e0 ldi r27, 0x00 ; r27 = 0
22c: a0 5a subi r26, 0xA0 ; r27-r26, 16bit, 計算指針指向 USBKeyID_table 和 M 對應的數字
22e: bf 4f sbci r27, 0xFF ; r27-r26, 16bit, 計算指針指向 USBKeyID_table 和 M 對應的數字
230: 8c 91 ld r24, X ; X = r27-r26, 16bit 指針, 讀取USBKeyID_table的內的按鍵號碼, 存入 r24
232: 80 93 86 00 sts 0x0086, r24 ; 按鍵號碼存入按鍵緩衝區, USB會[自動]發送, 等同人手按鍵
---------------翻譯後的 C 語言程序
這段 C 語言程序, 翻譯成機器碼後, 在列表看到的第一行, 最前面是 20c, 最後一行是 230, 大概計算一下,
0x230 - 0x20c = 0x24, 把這個十六進制數目換算成人類喜歡看的十進制數目,
0x24 = 36, 意思就是說, 這段 C 語言程序, 為了把一個十六進制數目, 例如 0x1F 轉換成 1, F, 必須耗用 36 BYTE 的空間做為算法的處理, 另外還有16 bytes 的對照表.
這裡就是所謂的折衷和妥協 -
原來用 10bit 精度, 現在只要 8bit;
原來用十進制顯示結果, 現在用十六進制標示.
這段 C 語言程序, 翻譯成機器碼後, 在列表看到的第一行, 最前面是 20c, 最後一行是 230, 大概計算一下, 0x230 - 0x20c = 0x24, 把這個十六進制數目換算成人類喜歡看的十進制數目, 0x24 = 36, 意思就是說, 這段 C 語言程序, 為了把一個十六進制數目, 例如 0x1F 轉換成 1, F, 必須耗用 36 BYTE 的空間做為算法的處理, 另外還有16 bytes 的對照表.
雖然結果不容易閱讀, 因為多數人習慣十進制, 不過可以節省超過 40 bytes, 的空間, 整個內容就可以塞在 2K 空間內.
十六進制並非外國人或搞電腦的專利, 買菜還用一斤等於16兩秤是有原因的, 中國人的度量衡就是有一斤16兩這樣的十六進制. 孔子在 [系辭上傳]說明, 易有太極, 是生兩儀, 兩儀生四象, 四象生八卦, 這個就是一分為二,二分為四,四分為八, 八分為十六的八卦的數目系統, 完全就是電腦的0和1的二進制系統, 所以日有新知, 只是因為久坐井底, 今日能夠理解, 完全是因為跳出觀天, 並非新奇事 (橙!?)
結語 -
無論 C 語言和 C Compiler 設計的多麼好, 能夠直接寫機器碼或 Mnamonic, 對於很在乎幾個百分點的容量限制來說, 沒有比好 Assembly Language (Mnamonic programming) 的設計來的有效率, 但是必須先要能夠理解個別處理器的特點及能力. 這次的實驗, 減肥成功.
留言列表