【广东龙芯2K500先锋板试用体验】驱动OLED小屏播放视频
一、硬件准备
硬件部分主要包括:
OLED屏幕参数:
- 驱动芯片:SSD1306
- 分辨率:128x64
- 接口:I2C
开发板选择使用I2C1,和OLED屏接线参考下图:
开发板和OLED小屏的连接关系为:
- 3号针(I2C1_SCL)连接到OLED屏的SCL脚
- 4号针(I2C1_SDA)连接到OLED屏的SDA脚
- 23号针(GND)连接到OLED屏的GND脚
- 24号针(P3V3)连接到OLED屏的VCC脚
二、背景知识
开始之前,先简单介绍一些背景知识。
2.1 Linux内核I2C驱动配置
龙芯2K0500内核默认已经打开了I2C驱动,启动后使用如下命令可以看到:
ls /dev/i2c-*
已经有i2c设备了。
2.2 Linux用户空间I2C API
参考这个文档:https://www.kernel.org/doc/html/latest/i2c/dev-interface.html
用户空间使用I2C,首先需要包含头文件:
#include <linux/i2c-dev.h>
#include <linux/i2c.h>
然后,打开设备文件:
int file;
int adapter_nr = 2;
char filename[20];
snprintf(filename, 19, "/dev/i2c-%d", adapter_nr);
file = open(filename, O_RDWR);
if (file < 0) {
exit(1);
}
打开设备之后,需要指定需要通信的从设备地址:
int addr = 0x40;
if (ioctl(file, I2C_SLAVE, addr) < 0) {
exit(1);
}
好了,接下来就可以进行I2C通信了:
buf[0] = reg;
buf[1] = 0x43;
buf[2] = 0x65;
if (write(file, buf, 3) != 3) {
}
if (read(file, buf, 1) != 1) {
} else {
}
以上几个代码段,都来自于kernel.org的文档。
三、移植SSD1306驱动库
3.1 选择SSD1306驱动库
之前移植过的一个STM32的SSD1306驱动库,原始开源项目链接:https://github.com/afiskon/stm32-ssd1306
移植后的开源项目连接: https://gitee.com/hihopeorg/harmonyos-ssd1306
这个移植版本主要修改包括:
- 适配了OpenHarmony 1.0的WIFI_IOT硬件接口;
- 添加了一个用于绘制矩形位图的接口,可用于绘制汉字;
- 优化了I2C全屏刷新速率;
这里使用移植版本作为基础。
3.2 移植SSD1306驱动库
主要修改点包括:
- 初始化函数ssd1306_Reset中,添加打开I2C设备的代码;
- 发送数据函数ssd1306_SendData中,修改为使用I2C用户空间接口的代码;
- 延时函数HAL_Delay,修改为使用ulseep实现;
- 计时函数HAL_GetTick,修改为使用gettimeofday实现;
- 添加了关闭函数ssd1306_Finish,用于关闭初始化时打开的I2C设备;
修改之后,这几个函数的主要代码为:
static int g_i2c = -1;
static uint64_t g_start_ms = 0;
#define TV2MS(tv) ((tv).tv_sec * 1000 + (tv).tv_usec / 1000)
void ssd1306_Reset(void)
{
char path[128] = {0};
snprintf(path, sizeof(path), "/dev/i2c-%d", SSD1306_DEV_NO);
g_i2c = open(path, O_RDWR);
if (g_i2c < 0) {
printf("open %s failed, %s!\n", path, strerror(errno));
}
if (ioctl(g_i2c, I2C_SLAVE, SSD1306_DEV_ADDR) < 0) {
printf("ioctl %s I2C_SLAVE failed, %s!\n", path, strerror(errno));
exit(1);
}
struct timeval start_tv = {0};
if (gettimeofday(&start_tv, NULL) != 0) {
printf("gettimeofday failed!\n");
}
g_start_ms = TV2MS(start_tv);
}
void ssd1306_Finish(void)
{
if (g_i2c >= 0) {
close(g_i2c);
}
}
void HAL_Delay(uint32_t ms)
{
usleep(ms * 1000);
}
uint32_t HAL_GetTick(void)
{
struct timeval now_tv = {0};
if (gettimeofday(&now_tv, NULL) != 0) {
printf("gettimeofday failed!\n");
}
return TV2MS(now_tv) - g_start_ms;
}
uint32_t HAL_GetTickFreq(void)
{
return 1000;
}
static uint32_t ssd1306_SendData(uint8_t* data, size_t size)
{
struct i2c_msg msg = {0};
msg.addr = SSD1306_DEV_ADDR;
msg.buf = data;
msg.len = size;
if (g_i2c >= 0) {
struct i2c_rdwr_ioctl_data data = {0};
data.msgs = &msg;
data.nmsgs = 1;
return ioctl(g_i2c, I2C_RDWR, &data) < 0 ? -1 : 0;
}
return -1;
}
3.3 添加CMake构建规则文件
接下来添加CMake构建规则CMakeLists.txt文件,分别到ssd1306目录和examples目录。
ssd1306目录的CMakeLists.txt用于编译驱动库,内容为:
set(sources
ssd1306.c
ssd1306_fonts.c
)
add_library(ssd1306 STATIC ${sources})
include_directories(.)
3.4 移植SSD1306测试程序
之前移植版的测试程序适配的是OpenHarmony 1.0,这里也需要修改,主要修改点:
- ssd1306_demo.c文件中,移除和OpenHarmony相关的代码;
- 添加main函数作为入口;
examples目录的CMakeLists.txt用于编译测试程序,内容为:
set(sources
ssd1306_demo.c
ssd1306_tests.c
)
add_executable(oled_test ${sources})
add_definitions(-DUSE_MAIN)
target_link_libraries(oled_test ssd1306)
target_link_libraries(oled_test m)
include_directories(../ssd1306)
3.5 LoongArch CMake构建参数
顶层的CMakeLists.txt文件内容如下:
cmake_minimum_required(VERSION 3.21.0)
set(CMAKE_SYSTEM_PROCESSOR loongarch)
set(CMAKE_C_COMPILER loongarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER loongarch64-linux-gnu-g++)
set(CMAKE_C_FLAGS -Wall)
project(ssd1306_oled)
add_subdirectory(ssd1306)
add_subdirectory(examples)
由于这里我添加了CMAKE_SYSTEM_PROCESSOR、CMAKE_C_COMPILER、CMAKE_CXX_COMPILER三个参数,因此可以直接编译出LoongArch
的可执行程序了。
如果不在CMakeLists.txt文件中指定这几个参数,通过命令行参数指定也是可以的:
cmake -B build -DCMAKE_SYSTEM_PROCESSOR=loongarch -DCMAKE_C_COMPILER=loongarch64-linux-gnu-gcc -DCMAKE_CXX_COMPILER=loongarch64-linux-gnu-g++
3.6 编译、运行SSD1306测试程序
完成以上步骤后,就可以编译SSD1306测试程序了。
编译:
cmake -B build
cmake --build build
编译完成后,build/examples目录下生成了oled_test二进制文件,将其拷贝到开发板上。
运行测试程序:
./oeld_test
不出意外的话,就可以看到OLED上正常显示各种测试画面了:
四、实现SSD1306播放视频
4.1 准备视频文件
首先需要准备一个视频文件,例如,我这里找的是蔡徐坤的“鸡你太美”视频;
4.2 转换视频格式
前面测试发现最大帧率接近 8 fps,接下来需要使用ffmpeg将视频转换为帧率 8 fps。
转换命令为:
ffmpeg -i input.mp4 -r 10 output.mp4
之后再使用Python脚本将视频转换为原始帧的二进制文件:
./video2bin.py output.mp4 out.bin
这里的bin文件包含若干个连续的原始帧数据,每个原始帧占用1KB(128x64/8=1024);
完整的视频转换python脚本,
import sys
import cv2 as cv
TARGET_WIDTH = 128
TARGET_HEIGHT = 64
PIXEL_PER_BYTE = 8
WIDTH_BYTES = int(TARGET_WIDTH/PIXEL_PER_BYTE)
PIXEL_THRESHOLD = 128.0
def pack_pixels(pixels, threshold):
value = 0
for gray in pixels:
bit = 1 if gray >= threshold else 0
value = (value << 1) + bit
return value
frameCount = 0
def resize_and_binarize_image(frame, width, height, threshold):
data = []
frame = cv2.resize(frame, (width, height))
frame = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
_, binary = cv2.threshold(frame, threshold, 255, cv2.THRESH_BINARY)
for r in range(height):
for b in range(int(width / PIXEL_PER_BYTE)):
colStart = b * PIXEL_PER_BYTE
pixels = frame[r, colStart: colStart + PIXEL_PER_BYTE]
byte = pack_pixels(pixels, threshold)
data.append(byte)
return bytes(data)
def convert_frame_to_bytes(frame):
return resize_and_binarize_image(frame, TARGET_WIDTH, TARGET_HEIGHT, PIXEL_THRESHOLD)
def convert_video_to_bin(videoFile, binFile):
cap = cv.VideoCapture(videoFile)
frameCount = cap.get(cv.CAP_PROP_FRAME_COUNT)
print('frame count:', frameCount)
print('frame width:', cap.get(cv.CAP_PROP_FRAME_WIDTH))
print('frame height:', cap.get(cv.CAP_PROP_FRAME_HEIGHT))
lastPercent = 0
with open(binFile, 'wb+') as f:
while True:
retval, frame = cap.read()
if not retval:
print('video done!')
break
bitmap = convert_frame_to_bytes(frame)
f.write(bitmap)
pos = cap.get(cv.CAP_PROP_POS_FRAMES)
percent = pos / frameCount * 100
if percent - lastPercent >= 1:
lastPercent = percent
sys.stdout.write('=')
sys.stdout.flush()
print('convert all frames done!')
cap.release()
def main():
if len(sys.argv) < 3:
print("Usage: {} videoFile binFile\n\t".format(sys.argv[0]))
exit(-1)
try:
videoFile = sys.argv[1]
binFile = sys.argv[2]
convert_video_to_bin(videoFile, binFile)
except Exception as e:
print('exception raised:', e)
if __name__ == "__main__":
main()
4.3 实现视频播放
在examples目录下,添加ssd1306_play.cpp文件,代码如下:
#include "ssd1306.h"
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <memory>
int play(char* video_bin)
{
std::unique_ptr<FILE, decltype(&fclose)> fptr{fopen(video_bin, "rb"), fclose};
uint32_t count = 0;
uint8_t frame[SSD1306_BUFFER_SIZE] = {0};
ssd1306_Init();
uint32_t beg = HAL_GetTick();
for(;;) {
size_t nbytes = fread(frame, 1, sizeof(frame), fptr.get());
if (ferror(fptr.get())) {
printf("Error: %s\n", strerror(errno));
return -1;
}
if (feof(fptr.get())) {
break;
}
ssd1306_Fill(Black);
ssd1306_DrawBitmap(frame, sizeof(frame));
ssd1306_UpdateScreen();
count++;
}
uint32_t end = HAL_GetTick();
ssd1306_Fill(Black);
ssd1306_UpdateScreen();
ssd1306_Finish();
float cost = (end - beg) / (float) HAL_GetTickFreq();
printf("Total frames : %d\n", count);
printf("Total time(s): %.3f\n", cost);
printf("Average FPS : %.3f\n", count / cost);
return 0;
}
int main(int argc, char* argv[])
{
if (argc <= 1) {
printf("Usage: %s video.bin\n", argv[0]);
return 1;
}
return play(argv[1]);
}
这段代码实现了播放原始视频二进制文件;
4.4 添加构建规则
examples目录的CMakeLists.txt中添加:
add_executable(oled_play ssd1306_play.cpp)
target_link_libraries(oled_play ssd1306)
include_directories(../ssd1306)
4.5 播放视频文件
完成以上操作后,重新编译,再次运行:
./oled_play ikun.bin
效果如下(B站视频):https://www.bilibili.com/video/BV1Gv4y1i7nW/
也可以看本贴底部视频;
五、源码仓库
本文所有代码均已在码云开源,链接为:https://gitee.com/swxu/linux-ssd1306
六、参考链接
- Implementing I2C device drivers in userspace — The Linux Kernel documentation: https://www.kernel.org/doc/html/latest/i2c/dev-interface.html
- 一个STM32 SSD1306驱动库:https://github.com/afiskon/stm32-ssd1306
- 【只因太美】用龙芯2K0500驱动小屏放视频: https://www.bilibili.com/video/BV1Gv4y1i7nW/