NUC980 執行 RT-Thread 驅動 SPI 介面 OLED 播放 badapple

哈拎發表於2021-07-05

badapple 是什麼,上網隨便查了下,沒看出個究竟,不過有個關於這個挺火的標籤或者主題 《 有螢幕的地方就有 badapple 》,網上有很多人用很多方式播放 badapple 動畫,有用微控制器在 OLED、LCD、LED點陣播放,有在 PC 上用各種程式語言、以各種形式播放,還有在示波器上播放。我之前也玩過

我用到的 badapple 資料是從這裡 OLED-STM32 找到的,在這裡,badapple 動畫資料被做成了一個二進位制檔案,因為是在一個 128 X64 的OLED上播放,badapple 資料被做成了一幀是128 X 64 個點的單色點陣圖形式,可以直接寫入 OLED上顯示,看了下這個檔案:

有 5.13 M byte,一幀是 128 X 64 / 8 = 1024 位元組的話,就有 5383168 /1024 = 5257 幀了。

用微控制器來播放,微控制器內部 Flash 一般都比較小,無法把 badapple 資料放入微控制器自帶的 Flash 中,所以用微控制器播放的話,大概就 2 種思路:

  • 把 badapple 放到外部儲存,如 SPI Flash、SD 卡等,微控制器從這些儲存裝置讀出資料,然後顯示到顯示器上,
  • PC 或者 手機 等把 badapple 資料通過通訊介面傳送給 MCU,通訊介面可以是UART、USB、網路、藍芽等,微控制器從這些通訊介面接收到資料然後顯示到顯示器上

這裡嘗試把 badapple 資料編譯進 nuc980 韌體中,這樣的話,編譯出來的韌體至少 5.13 M位元組,對於只能用內部flash儲存韌體的微控制器來說的話,應該是不行的,我還沒見到過內部flash有這麼大的微控制器。對於 NUC980 可以用外部flash、有這麼大的 DRAM,是完全可以實現的,就像之前說的,為所欲為了,如果 NUC980 跑 Linux 的話,這5點幾兆算小了。

這裡要做的有:

  • 先把顯示器驅動起來,
  • 解決怎麼把 badapple 資料編譯進韌體

硬體

我這裡用到的顯示器是 SPI 介面的 OLED 模組,驅動晶片是 SSD1306,如下:

既然是使用 SPI介面,就要用到 NUC980 的SPI介面了,上圖,還是這樣用過了好幾次的圖,NuMaker-RTU-NUC980 板子引出的 IO:

可以看到板子上引出了 SPI0,還需要 2 個 GPIO 用於 OELD的 RST、DC,接線如下:

OLED     NUC980
D0	<--  PC6
D1	<--  PC8
RST	<--  PB4
DC	<--  PB6
CS	<--  PC5

實物如下:

把 OLED 驅動起來

RT-Thread 中 SPI 介面也有相應的驅動框架、對於的API,具體可以檢視 RT-Thread 相應文件 -- SPI裝置

首先定義一個 spi 裝置,然後掛載到 SPI0,設定 SPI 引數,並把另外用到的 2 個 IO 設定為輸出,如下:

static struct rt_spi_device spi_dev_lcd;

#define SSD1306_DC_PIN  NU_GET_PININDEX(NU_PB, 6)
#define SSD1306_RES_PIN NU_GET_PININDEX(NU_PB, 4)

static int rt_hw_ssd1306_config(void)
{
    if (rt_spi_bus_attach_device(&spi_dev_lcd, "lcd_ssd1306", "spi0", RT_NULL) != RT_EOK)
        return -RT_ERROR;

    /* config spi */
    {
        struct rt_spi_configuration cfg;
        cfg.data_width = 8;
        cfg.mode = RT_SPI_MASTER | RT_SPI_MODE_3 | RT_SPI_MSB;
        cfg.max_hz = 42 * 1000 * 1000; /* 42M,SPI max 42MHz,lcd 4-wire spi */

        rt_spi_configure(&spi_dev_lcd, &cfg);
    }

    rt_pin_mode(SSD1306_DC_PIN, PIN_MODE_OUTPUT);
    rt_pin_mode(SSD1306_RES_PIN, PIN_MODE_OUTPUT);

    return RT_EOK;
}

然後實現對 OLED 寫命令、資料函式:

static rt_err_t ssd1306_write_cmd(const rt_uint8_t cmd)
{
    rt_size_t len;

    rt_pin_write(SSD1306_DC_PIN, PIN_LOW);

    rt_spi_transfer(&spi_dev_lcd, (const void *)&cmd, NULL, 1);
    if (len != 1)
    {
        LOG_I("ssd1306_write_cmd error. %d", len);
        return -RT_ERROR;
    }
    else
    {
        return RT_EOK;
    }
}

static rt_err_t ssd1306_write_data(const rt_uint8_t data)
{
    rt_size_t len;

    rt_pin_write(SSD1306_DC_PIN, PIN_HIGH);
    rt_spi_transfer(&spi_dev_lcd, (const void *)&data, NULL, 1);

    if (len != 1)
    {
        LOG_I("ssd1306_write_data error. %d", len);
        return -RT_ERROR;
    }
    else
    {
        return RT_EOK;
    }
}

實現 OLED 初始化、寫屏、清屏函式:

void ssd1306_fill(uint8_t *dat)
{
	uint8_t i,n;		    
	for(i=0;i<8;i++)  
	{  
		ssd1306_write_cmd (0xb0+i);   
		ssd1306_write_cmd (0x00);      
		ssd1306_write_cmd (0x10);      
		for(n=0;n<128;n++)
        {
            ssd1306_write_data(*dat); 
            dat ++;
        }
            
	}   
}

void ssd1306_clear(void)  
{  
	uint8_t i,n;		    
	for(i=0;i<8;i++)  
	{  
		ssd1306_write_cmd (0xb0+i);   
		ssd1306_write_cmd (0x00);      
		ssd1306_write_cmd (0x10);      
		for(n=0;n<128;n++)
        {
            ssd1306_write_data(0x00); 
            dat ++;
        }
            
	} 
}


static int rt_hw_ssd1306_init(void)
{
    
    rt_hw_ssd1306_config();

    rt_pin_write(SSD1306_RES_PIN, PIN_HIGH);
    rt_thread_delay(RT_TICK_PER_SECOND / 10);
    rt_pin_write(SSD1306_RES_PIN, PIN_LOW);
    //wait at least 100ms for reset
    rt_thread_delay(RT_TICK_PER_SECOND / 10);
    rt_pin_write(SSD1306_RES_PIN, PIN_HIGH);

	ssd1306_write_cmd(0xAE); 
	ssd1306_write_cmd(0xD5); 
	ssd1306_write_cmd(80);  
	ssd1306_write_cmd(0xA8);
	ssd1306_write_cmd(0X3F);
	ssd1306_write_cmd(0xD3); 
	ssd1306_write_cmd(0X00);

	ssd1306_write_cmd(0x40); 
													    
	ssd1306_write_cmd(0x8D); 
	ssd1306_write_cmd(0x14); 
	ssd1306_write_cmd(0x20); 
	ssd1306_write_cmd(0x02); 
	ssd1306_write_cmd(0xA1); 
	ssd1306_write_cmd(0xC0);
	ssd1306_write_cmd(0xDA); 
	ssd1306_write_cmd(0x12); 
		 
	ssd1306_write_cmd(0x81); 
	ssd1306_write_cmd(0xEF);
	ssd1306_write_cmd(0xD9); 
	ssd1306_write_cmd(0xf1); 
	ssd1306_write_cmd(0xDB); 
	ssd1306_write_cmd(0x30); 

	ssd1306_write_cmd(0xA4); 
	ssd1306_write_cmd(0xA6); 
	ssd1306_write_cmd(0xAF); 

    ssd1306_clear();

    return RT_EOK;
}
INIT_COMPONENT_EXPORT(rt_hw_ssd1306_init);

把 badapple 資料編譯進韌體(1)

首先想到的是最常用的方法,把 badapple 資料放在一個陣列裡,然後把該陣列的內容寫入 OLED 就行了,因為 badapple 資料是二進位制,轉成陣列形式的話,放在程式碼裡面,就相當於把二進位制轉成字串,比如以下資料:

對於第一行,轉成陣列形式的話,如下:

uint8_t example[] = {0x23,0x78,0x33,0xb9,0x04,0x4b,0x13,0xb1,0x04,0x48,0xaf,0xf3,0x00,0x80,0x01,0x23};

如果數量不多的話,還好手動轉換下,可是對於這個有 5 點幾兆的,也就是有 5383168 位元組,上百萬個位元組,手動轉換肯定不實際,也沒去找有沒有什麼現成工具可以用,我自己用 python 寫了個小程式,把這個轉換出來了,來感受下:

看了下,有30多萬行,我電腦開啟這檔案都會有點卡頓,

經過這麼一頓操作,我可是程式碼量超過 10萬行的了,還是輕輕鬆鬆、隨隨便便就達到了。

把該檔案放到工程裡面,然後寫個播放 badapple 的函式:

int badappple_test(int argc, char **argv)
{
    uint32_t len =0,frame = 0,i=0,index = 0;
    rt_tick_t start = 	rt_tick_get();
    len = sizeof(badapple);
    frame = len / 1024;
    
    for(i=0;i<frame;i++)
    {
        ssd1306_fill(&badapple[i * 1024]);
    }
    rt_tick_t end = 	rt_tick_get();
    rt_kprintf("Frame:%d\r\n",frame);
    rt_kprintf("start:%d, end:%d, use:%d\r\n",start,end,end-start);
    rt_kprintf("1 s tick:%d\r\n",rt_tick_from_millisecond(1000));
}
MSH_CMD_EXPORT(badappple_test, badappple test);

開始的時候獲取滴答定時器當前的值:

rt_tick_t start = 	rt_tick_get();

然後獲取 badapple 資料對應的陣列的長度,計算幀數:

    len = sizeof(badapple);
    frame = len / 1024;

然後就是寫屏了:

    for(i=0;i<frame;i++)
    {
        ssd1306_fill(&badapple[i * 1024]);
    }

最後獲取結束的時候的滴答定時器的值,用於計算幀率:

  rt_tick_t end = 	rt_tick_get();
  rt_kprintf("Frame:%d\r\n",frame);
  rt_kprintf("start:%d, end:%d, use:%d\r\n",start,end,end-start);
  rt_kprintf("1 s tick:%d\r\n",rt_tick_from_millisecond(1000));

編譯,看下編譯出來的韌體大小:

也是有 5 點幾兆,最後執行效果為:

串列埠終端輸出的資訊:

可以看到播放持續了 23.238 秒,可以計算出幀率為:

5257 / 23.238 = 226.22

226 幀每秒,這會不會是全網用微控制器播放 badapple 中幀率最高的呢,

把 badapple 資料編譯進韌體(2)

對於第一種方法,由於要經過轉換,有點麻煩,突然想到之前看到過一套 GUI 原始碼,程式碼工程裡面是直接把二進位制進韌體,忘記具體是怎麼實現的,上網搜了下,實驗了幾次,居然做出來了,具體做法如下。

實現這個功能的關鍵是 incbin 指令,該指令是一條 arm 偽指令,功能是把一個檔案包含到當前的原始檔中,編譯的時候會把該檔案以二進位制的形式編譯進韌體中。由於該指令是彙編,所以需要建立一個 .s 檔案,內容為:

  .section .rodata
  .global BADAPPLE_FILE 
  .type   BADAPPLE_FILE, %object 
  .align 4
BADAPPLE_FILE:       
  .incbin "applications/badapple.bin"
BADAPPLE_FILE__END:
  .global BADAPPLE_FILE_SIZE
  .type   BADAPPLE_FILE_SIZE, %object
  .align 4
BADAPPLE_FILE_SIZE:
  .int BADAPPLE_FILE__END - BADAPPLE_FILE 

這裡定義了 2 個全域性變數 BADAPPLE_FILE、BADAPPLE_FILE_SIZE,BADAPPLE_FILE 是 badapple 資料開始位置,相當於一個陣列頭位元組地址,BADAPPLE_FILE_SIZE,是 badapple 資料大小。把該 .s 檔案 跟 badappel.bin 同時放到 工程中的 applications 目錄下,還要修改下 applications 的 SConscript 檔案,因為預設是沒有編譯該目錄下的 .s 檔案,修改為:

# RT-Thread building script for component

from building import *

cwd = GetCurrentDir()
src = Glob('*.c') + Glob('*.cpp')  + Glob('*.s')
CPPPATH = [cwd, str(Dir('#'))]

group = DefineGroup('Applications', src, depend = [''], CPPPATH = CPPPATH)

Return('group')

接下里就實現播放 badapple 函式:

extern const uint8_t BADAPPLE_FILE;
const uint8_t *badapple_p = &BADAPPLE_FILE;
extern uint32_t BADAPPLE_FILE_SIZE;
int badappple_test(int argc, char **argv)
{
    uint32_t frame = 0,i=0,index = 0;
    rt_tick_t start = 	rt_tick_get();
    frame = BADAPPLE_FILE_SIZE / 1024;
    
    for(i=0;i<frame;i++)
    {
        ssd1306_fille((uint8_t *)&badapple_p[i * 1024]);
    }
    rt_tick_t end = 	rt_tick_get();
    rt_kprintf("file size is:%d",BADAPPLE_FILE_SIZE);
    rt_kprintf("Frame:%d\r\n",frame);
    rt_kprintf("start:%d, end:%d, use:%d\r\n",start,end,end-start);
    rt_kprintf("1 s tick:%d\r\n",rt_tick_from_millisecond(1000));
}
MSH_CMD_EXPORT(badappple_test, badappple test);

編譯執行,經過測試,效果跟上一個方法是一樣的。

轉載請註明出處:https://www.cnblogs.com/halin/

相關文章