3

Coding4Fun - nanoFramework OLED 效能測試與調校

 2 years ago
source link: https://blog.darkthread.net/blog/nf-ssd1306-perf-tuning/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
nanoFramework OLED 效能測試與調校-黑暗執行緒

前兩週用 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# 了,別太貪心。哈!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK