痞子衡嵌入式半月刊: 第 54 期 這裡分享嵌入式領域有用有趣的項目/工具以及一些熱點新聞,農曆年分二十四節氣,希望在每個交節之日準時發佈一期。 本期刊是開源項目(GitHub: JayHeng/pzh-mcu-bi-weekly),歡迎提交 issue,投稿或推薦你知道的嵌入式那些事兒。 上期回顧 ...
介紹
本文介紹在51單片機上,使用OLED12864(SSD1306)播放視頻,並且使用蜂鳴器播放音樂。
(因為是gif的原因,看著會比較卡,實際上是不會有卡頓的,實際效果可以看文末的視頻鏈接)
最終的效果如下:
播放bad apple的效果:
播放數位寶貝的效果:
使用到的主要元器件如下:
-
國產51單片機:STC15F2K60S2
-
OLED顯示屏:SSD1306,解析度為128*64
-
無源蜂鳴器,8550三極體等
原理圖如下:
具體方案
由於視頻文件比較大(MB級別),而51單片機的flash一般都比較小(KB級別),因此把視頻文件直接存儲在單片機內部顯然是不行的。可以把視頻文件存儲在SD卡裡面,然後單片機讀取SD卡裡面的內容;或者視頻文件直接存儲在電腦上,然後電腦通過串口實時發送視頻數據給單片機,單片機實時顯示視頻畫面。文本採用後者的方案。
OLED12864繪圖
我們購買OLED12864(SSD1306)顯示屏時,一般賣家都會提供51單片機的示例代碼,或者網上也能找到很多相關的代碼。
使用這些代碼在整個屏幕上繪圖時,發現刷新率比較低,在11.0592M時鐘頻率的情況下,實測大概只有8.6fps。測試方法如下:
void main() { for(;;) { p27 = ~p27; oled_drawbmp(pic); } }
oled_drawbmp為賣家提供的繪圖的函數,每次屏幕刷新一次,p27 IO口翻轉一次。使用邏輯分析儀測試p27的電平變化如下,可以看到頻率約為4.3Hz,那麼屏幕的刷新率大概為8.6Hz(fps)。
因為我們的目的是使用單片機在這款顯示屏上播放視頻,而一般視頻的幀率需要大於25fps,幀率過低就會有卡頓的感覺。顯然,上面提到屏幕8.6Hz的刷新率是比較低的,因此我們需要做一些優化。
比較直觀且容易的優化方式之一,就是提高時鐘頻率,把11.0592M提高到24M或者27M的時鐘頻率。
第二個優化方法就是優化繪圖函數。
先來看看iic的開始信號和結束信號的代碼:
void iic_start() //開始iic { scl = 1; sda = 1; delay_5us(); sda = 0; delay_5us(); scl = 0; } void iic_stop() //停止iic { scl = 0; sda = 0; scl = 1; delay_5us(); sda = 1; delay_5us(); }
可以看到,裡面有一些延時5微妙(delay_5us),其實這個不是必須的,去掉這個延時,iic同樣可以正常通信。因此,去掉這個延時,可以加快顯示屏的刷新速率。
其次,我們再看看賣家提供繪圖部分的函數:
/***********功能描述:顯示顯示BMP圖片128×64起始點坐標(x,y)*****************/ void oled_drawbmp(unsigned char bmp[]) //畫圖 { unsigned int j = 0; unsigned char x, y; for (y = 0; y < 8; y++) { oled_set_pos(0, y); for (x = 0; x < 128; x++) { iic_writedata(bmp[j++]); } } }
而iic_writedata的實現如下:
void iic_writedata(unsigned char iic_data) //寫數據 { iic_start(); write_byte(0x78); write_byte(0x40); write_byte(iic_data); iic_stop(); }
可以看到,每寫一次圖像數據bmp[j],都會有一次iic開始與結束動作,也都會先發送兩個控制指令(0x78, 0x40),這其實沒有必要,優化後的函數如下:
// 快速繪製圖像 void oled_drawbmp_fast(unsigned char BMP[]) { unsigned int j = 0; unsigned char x, y; for (y = 0; y < 8; y++) { oled_set_pos(0, y); iic_start(); write_byte(0x78); write_byte(0x40); for (x = 0; x < 128; x++) { write_byte(BMP[j++]); } iic_stop(); } }
可以看到,上面的函數減少了啟動iic、結束iic,減少了寫控制命令(0x78, 0x40)。使用跟之前同樣的測試方法,經過上述的優化,最終屏幕的刷新率如下:
上述的結果為,使用27M時鐘頻率,加上上面提到的幾點優化,可以看出最終屏幕的刷新率約為34.5*2=69Hz(fps),這已經滿足我們播放視頻所需的屏幕刷新率了。
另外,再提一點,其實還可以進一步優化,使得屏幕刷新率達到100fps以上,測試結果如下。在這裡賣個關子,感興趣可以去B站看下,視頻地址為:51單片機播放視頻-原理介紹。
視頻轉碼成十六進位格式
單片機播放視頻,我們需要將視頻轉碼為單片機可以讀取的十六進位數據。
首先我們需要將視頻分解為一幀一幀的圖像,然後可以用如下的取模軟體獲得圖像的十六進位字模。
但是,由於視頻的幀數比較多,我們一幀一幀手動的使用取模軟體獲取字模,顯然是一個比較累的活。因此,我們可以寫個python代碼,批量生成每幀畫面的十六進位數據。python代碼如下:
import cv2 def bit2num(pixcels): output_val = 0 for i, pix in enumerate(pixcels): if pix > 128: # 白色 output_val += pow(2, i) return output_val def main(): video_path = 'BadApple.flv' cap = cv2.VideoCapture(video_path) # 打開視頻 cnt = 0 fout = open('bad_apple_data.txt', 'w') # while True: ret, frame = cap.read() # 一幀一幀的讀取 if not ret: break cnt += 1 frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 轉換為灰度的畫面 frame = cv2.resize(frame, (128, 64)) # 圖像尺寸調整到128*64大小 convert_val = [] for row in range(0, 8): # page0 ~ page7 for col in range(0, 128): # seg0 ~ seg127 cur_data = frame[row*8: row*8+8, col] # 取出對應的8個像素點 convert_val.append(str(bit2num(cur_data))) # 轉換成8位的數據 fout.write("%s\n"%(','.join(convert_val))) # cv2.imshow("capture", frame) #顯示畫面 # if cv2.waitKey(30) & 0xff == ord('q'): #按q退出 # break main()
代碼比較簡單,其中會調用OpenCV的庫,用來讀取視頻以及視頻畫面對應的像素值。
串口發送
視頻數據準備好之後,我們需要把視頻數據通過串口發送給單片機,單片機接收到完整的一幀數據(一幀畫面)之後,就可以開始顯示畫面。我們同樣可以寫個python代碼來將視頻數據發送給單片機,代碼如下:
import serial # 導入串口相關的庫 from time import sleep def get_bmp_data(): filepath = 'bad_apple_data.txt' f = open(filepath) bmp_data = [] for line in f: val = line.strip().split(',') if len(val) == 0: continue bmp_data.append([int(x) for x in val]) f.close() return bmp_data def main(): com = serial.Serial('com10', 345600, timeout=10) # 設置埠號,波特率,超時時間 if not com.isOpen(): # 判斷埠是否打開成功 raise "埠打開失敗" bmp_data = get_bmp_data() # 讀取剛剛生成的TXT文件 for frame in bmp_data: # 一幀一幀的發送數據 ret = com.write(bytes(frame)) # 將數據轉換成二進位後發送 sleep(0.03) # 延時適當時間 main()
代碼比較簡單,其中會調用串口相關的serial庫,然後每次迴圈發送一幀數據,直至全部發送完成。
小結
最後,總結一下 在51單片機播放視頻的大致流程:
-
視頻解碼成一幀幀的圖像,然後再轉碼成顯示屏可以顯示的十六進位格式(這一步可以提前完成)
-
電腦通過串口把十六進位格式的視頻數據發送給單片機
-
單片機接收到完整的一幀數據(一幅圖像)後,顯示屏開始顯示畫面
-
同時,蜂鳴器播放音樂(可選)
註:
想進一步瞭解細節的朋友,可以去B站看下我上傳的視頻,裡面的介紹更加詳細一點,也有最終的效果演示。