Coding4Fun - nanoFramework OLED 效能測試與調校
0 | 1,773 |
前兩週用 VSCode 寫 Arduino C++ 完成 OLED 顯示器 I2C vs SPI 效能評測,得到 9.8s vs 1.2s 的評測結果。得到 I2C 的效能數據,下一個我最想知道的便是「改用 nanoFramework / C# 會慢多少?」。
基於語言特性,要拼效能,Python、.NET、PHP 這類高階語言都不會是 C/C++ 的對手,但究竟差距有多大,讓我好奇。然而,執行效能不是選擇程式語言的唯一考量,只要效能差距別太離譜,nanoFramework 具有內建資料型別、API 較豐富,開發環境成熟 (Visual Studio 既出,誰與爭鋒),提供記憶體及執行緒管理,對於效能要求不嚴苛的應用,能省下可觀的學習、開發及除錯時間。
花了點功夫,將 Arudino SSD1306 驅動程式庫翻寫成 C# 版,在 nanoFramework 復刻上次的天竺鼠 Logo 滑入動畫與 0-255 數字列印測試:
Ssd1306 oled = new Ssd1306();
oled.Initialize();
oled.ClearScreen();
for (int i = 128; i >= 32; i--)
{
oled.DrawImage(i, 0, 64, 64, guineapig_logo);
oled.Display();
}
oled.ScrollUpLine();
oled.Display();
oled.CursorY = 56;
for (int i = 0; i < 256; i++)
{
oled.Print($"{i} ");
}
oled.PrintLine();
TimeSpan dura = DateTime.UtcNow - st;
oled.PrintLine($"I2C Test: {dura.TotalMilliseconds:n0}ms");
第一次跑完我有點吐血,居然花了 61 秒。不過,沒關係,先分析慢在哪裡,才知怎麼改。nanoFramework 沒有 Stopwatch 可用,一不做二不休,我把之前發明的極簡風格 .NET Stopwatch 計時法搬進來,計時部分改用開始結束的 DateTime.UtcNow 相減取 TotalMilliseconds。先觀察像蝸牛爬的從右滑入動畫:
for (int i = 128; i >= 32; i--)
{
using (var ts1 = new TimeMeasureScope($"DrawImage at X:{i}"))
{
oled.DrawImage(i, 0, 64, 64, guineapig_logo);
using (var ts2 = new TimeMeasureScope("Send Data to SSD1306"))
{
oled.Display();
}
}
}
由 Debug Log 結果得知顯示一次圖要 359ms,其中 DrawImage() 是設定像素 byte[],耗時約 250ms,Dispaly() 是將 byte[1024] 透過 I2C 送到 OLED,耗時約 106ms:
...
DrawImage at X:37|359ms
Send Data to SSD1306|106ms
DrawImage at X:36|358ms
Send Data to SSD1306|106ms
DrawImage at X:35|359ms
Send Data to SSD1306|106ms
DrawImage at X:34|359ms
Send Data to SSD1306|106ms
...
先查 I2C 送資料部分,發現我犯了一個錯,我用到 I2cBusSpeed.StandardMode 而非 I2cBusSpeed.FastMode:
new I2cDevice(new I2cConnectionSettings(busId, i2cAddr, I2cBusSpeed.StandardMode));
修改之後,Send Data to SSD1306 降到 35ms。但總時間仍要 290ms,等於一秒才刷新三次,從 128 移到 32 超過 30 秒。DrawImage() 的寫法如下:
public void DrawImage(int x, int y, int w, int h, byte[] bitmap)
{
var rowByteCount = w / 8 + (w % 8 > 0 ? 1 : 0);
for (int i = 0; i < w; i++)
{
for (int j = 0; j < h; j++)
{
SetPixel(x + i, y + j, (bitmap[j * rowByteCount + i / 8] << (i % 8) & 0x80) > 0);
}
}
}
public void SetPixel(int x, int y, bool on = true)
{
if (x < 0 || x > WIDTH - 1) return;
if (y < 0 || y > HEIGHT - 1) return;
int index = (y / 8) * WIDTH + x;
if (index > buffer.Length - 1) return;
if (on)
buffer[index] = (byte)(buffer[index] | (byte)(1 << (y % 8)));
else
buffer[index] = (byte)(buffer[index] & ~(byte)(1 << (y % 8)));
}
四平八穩的寫法跟 C++ 版本相去不遠,卻有明顯速度差異,要加速就只能靠 unsafe 了。
查到在 .nfproj 加入 <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
nanoFramework 也能寫 unsafe,讓我高興了一下。不料,改好的程式雖然可以編譯,但執行會爆出奇怪錯誤,甚至直接閃退。
在 nanoFramework Discord 社群發問,得到熱心回答(還遇到 nanoFramework 創始成員,也是微軟 MVP 的 José Simões),用破爛雞同鴨講了一陣子,最後我確認一點:nanoFramework 雖然支援 unsafe,但能做的事還很侷限,不是所有動作都支援,低階 byte[] 指標存取便是其一,美夢破碎。結論是,目前 nanoFramework 遇到 byte[] 效率不佳還無法用 unsafe 加速。
不甘心在這裡就止步,我做了不理性的事 (正常人應該要放棄回去用 C++) - 花了時間想更複雜的演算法破突困境。
由測試過程可以看出我寫的文字列印速度比圖形顯示快很多,分析背後的原因是字型資料 byte[] 是採 Y 軸每 8 點存一個 Byte,與顯示器記憶體格式一致,故每搬一個 Byte 可以複製 8 個像素(必要時要拆兩部分分別寫入對映的上下 Byte);而圖案 Bitmap 則是 X 軸 8 點一個 Byte,故必須以像素為單位逐 Bit 處理。針對需反覆顯示的圖形,若預先將 Bitmap byte[] 也轉成 Y 軸 8 點一個 Byte,就能減少 Byte 寫入次數可由 8 次降到 2 次以下,儘可能避開 nanoFramework byte[] 存取比存標存取慢的大弱點。
當成演算法練習,搞了半天生出以下這段程式:
public byte[] FlipImageArray(byte[] bitmap, int w, int h)
{
var totalRowBytes = h / 8 + (h % 8 > 0 ? 1 : 0);
var data = new byte[w * totalRowBytes];
var rowByteCount = w / 8 + (w % 8 > 0 ? 1 : 0);
for (int i = 0; i < w; i++)
{
for (int j = 0; j < h; j++)
{
if ((bitmap[j * rowByteCount + i / 8] << (i % 8) & 0x80) > 0)
{
var offset = j / 8 * w;
data[offset + i] |= (byte)(1 << j % 8);
}
}
}
return data;
}
public void DrawFlipArrayImage(int x, int y, int w, int h, byte[] flipBitmap)
{
var baseIdx = y / 8 * WIDTH;
var yOffset = y % 8;
var hCount = h / 8 + (h % 8 > 0 ? 1 : 0);
for (int i = 0; i < w; i++)
{
if (x + i > WIDTH - 1) continue;
for (var j = 0; j < hCount; j++)
{
var pos = baseIdx + j * WIDTH + x + i;
if (pos > buffer.Length - 1) continue;
//取出 8bit 資料
var d = flipBitmap[j * w + i];
//最後一列可能有效範圍不足 8bit,把無效範圍遮掉
if (j == hCount - 1)
d &= (byte)(0xff >> (8 - h % 8) % 8);
//寫入本列
buffer[pos] = (byte)(buffer[pos] & 0xff >> 8 - yOffset | (d << yOffset & 0xff));
//寫入下一列
pos += WIDTH;
if (pos > buffer.Length - 1) continue;
buffer[pos] = (byte)(buffer[pos] & (0xff << yOffset & 0xff) | d >> 8 - yOffset);
}
}
}
令人興奮的時刻,執行時間由 41 秒推進到 18 秒!
雖然還是比 Arduino C++ 多一倍,這個速度我已可接受,但別想妄想用 nanoFramework 播動畫,寫掌上型遊樂器就是了,都讓你用 C# 了,別太貪心。哈!
Experience of tuning naneFramework code for SSD1306 OLED display.
Comments
Be the first to post a comment