代码地址如下:
http://www.demodashi.com/demo/13218.html

概述

本篇教程讲述了使用树莓派驱动OLED12864液晶屏,并在液晶屏上播放动画和视频.

硬件平台

  • 树莓派一台—RaspberryPi_2B。
  • OLED12864显示屏一块,SPI接口。

软件平台

  • wiringPi—开源树莓派GPIO库。
  • EasyBMP—开源BMP图片处理库(这个库是用C++编写的,主要为了方便提取BMP图片数据,我已经做好了数据提取的小工具,可以直接拿去用,不过我还是会贴出源代码,不会C++的朋友也不要着急)。
  • KMPlayer视频播放器—用于视频逐帧截图。
  • BadApple.mp4—用于此次在液晶屏上播放的视频(此视频是纯黑白的,对于只能显示”黑白”色的LED显示屏来说是绝佳的选择)。

效果展示

原理详解

12864液晶显示屏驱动编写

我这里采用的液晶屏是从淘宝上淘的1.3寸的OLED显示屏,像素点大小比平常大家用的那种大块头的12864(玩单片机的朋友都知道的)小很多,效果自然也要好很多。而且OLED是自发光的,不需要背光。如下图:

我用的这个显示屏是7线的,SPI接口,控制器是SH1106,你用的可能跟我的不一样,但是不要紧,只要把这个显示屏驱动起来就行。如果驱动代码不会写,就到网上去找一份51单片机的代码来改一改就好了,我就是这么做的,^_~,所以关于驱动这块我就只简单介绍一下。

我用的是wiripingPi这个库来驱动GPIO,就相当于把树莓派当一块普通的单片机来用,并没有使用Linux下的字符设备驱动框架,怎么简单怎么来。关于这个库的安装与使用大家就自行百度吧。下面这个例子大家已看就应该会用了:

#include <wiringPi.h>//包含头文件
int main(void)
{
wiringPiSetup();//初始化
pinMode(1, OUTPUT);//设置1脚为输出模式
while(1)
{
digitalWrite(1, HIGH);//1脚输出高电平
delay (100);//延时100毫秒(wiringPi库自带的延时函数)
digitalWrite(1, LOW);//1脚输出低电平
delay (100);//延时100毫秒
}
}

既然都能控制GPIO了,那接下来移植驱动不是分分钟的事了。找个51单片机的驱动来改改就完事了。这里我就不过多叙述了。其实我们只要能实现往液晶屏内部发送显示数据和命令的两个函数,一个定位函数,一个初始化函数就够了。其他的事都与硬件无关了。

在WiripingPi的源码目录里有一个12864显示屏的驱动范例代码,我就是在这份代码基础上该的。这份范例代码还提供了点阵字体和显示字符串的函数:

// /wiringPi-5edd177/devLib/lcd128x64.c
// /wiringPi 5edd177/devLib/lcd128x64.h
// /wiringPi-5edd177/devLib/font.h /*
* sentData:
* Send an data or command byte to the display.
*********************************************************************************
*/
static void sendData (int32 dat, const int32 cmd) /*
* setCol: setPos:
* Set the column and line addresses
*********************************************************************************
*/
static void setPos(const int32 x, const int32 y) /*
* lcd128x64update:
* Copy our software version to the real display
*********************************************************************************
*/
void lcd128x64update (void) /*
* lcd128x64setup:
* Initialise the display and GPIO.
*********************************************************************************
*/
int32 lcd128x64setup (void)

关于液晶绘图的补充说明

众所周知,使用液晶绘图(二维图形)的两个最基本的API就是set_point和get_point,但是有一点需要注意,set_point好说,所有的液晶硬件都支持,或者可以通过软件巧妙地实现,但是有些液晶硬件上是不支持get_point的,我所知道的就有常用的诺基亚5110的屏幕就不支持。但是这也不要紧,如果我们的单片机内存足够大,完全可以通过软件实现这一功能,不管硬件是否支持。

我在程序里开辟了一片内存区域用来当作”显存”,其实就是一个二维数组(一维也可以,不过计算要复杂点)把所有要显示的都先放到这块在程序内部定义的显存里,需要更新画面的时候再调用之前的lcd128x64update()函数把这片显存里的数据刷到显示屏上就行了。这样做就使得对硬件的操作变简单了,我们只需要硬件提供一个把显存内容更新到硬件中去显示的API就可以了,其他的操作完全在软件内实现。

这片显存的大小是128×8=1024个byte。每隔byte对应8个像素点,这和硬件有关,显示屏内部共有1024个8位的寄存器,每个寄存器控制8个像素点的亮和灭,而且这些寄存器是竖着排列的,每一行有128个,每一列有8个(与之对应的每一行有128个点,每一列有8×8=64个点),(主意我这里是为了方便大家理解才这么讲的,至于实际硬件是不是这样我也不敢保证,大家不要被我误解)请看下图:

图中每一个小格代表一个像素点,所以我们把显存数组定义成下面这种形式,以便和硬件一一对应:

// Size
#defineLCD_WIDTH 128
#defineLCD_HEIGHT 8 // Software copy of the framebuffer
static uint8 frameBuffer [LCD_WIDTH][LCD_HEIGHT];

比如如果像素点坐标用(x,y)分别表示列和行,那么(5, 8)就应该位于frameBuffer[5][1]这个显存单元(byte)中的第0个比特位(0-7,从低到高)。如果我们把frameBuffer[5][1] = 0x01 这个值送入到显示屏内部的(5,1)这个地址单元就能点亮(5, 8)这个像素点。如下图:

下面这个更新显示屏画面的函数现在应该能看懂了把:

/*
* lcd128x64update:
* Copy our software version to the real display
*********************************************************************************
*/ void lcd128x64update (void)
{
int32 x = 0, y = 0;
for(y = 0; y < (LCD_HEIGHT); y++)
{
setPos(0, y);
//每往显示屏中写入一次数据,该显示屏的列坐标就会自动向后加1,所以列坐标x不用每次都设置。只需要设置行坐标y。
for(x = 0; x < LCD_WIDTH; x++)
{
sendData(frameBuffer[x][y], OLED_DATA);
}
}
}

如果仅仅实现用液晶屏显示动画,上面所说的内容是不需要关注的,只需要明白怎么样把一幅图像显示到液晶屏上去就行了。但愿各位看官不要觉得我过于啰嗦。

显示一副图像

在上面,我们已经实现了几个硬件相关的API(关于液晶绘图的补充内容与本帖要实现的功能是无关的): 往液晶的控制器内发送指令和数据的API(sendData), 液晶显示定位的API(setPos). 有了这两个API我们就可以把一副图像显示到液晶上了. 具体的做法就是循环的依次把图像上的每一个像素点发送到液晶控制器中去. 参考代码 lcd128x64update

/*
* sentData:
* Send an data or command byte to the display.
*********************************************************************************
*/
static void sendData (int32 dat, const int32 cmd)
{
int32 i;
if(cmd)
{
OLED_DC_Set();
}
else
{
OLED_DC_Clr();
}
OLED_CS_Clr();
for(i = 0; i < 8; i++)
{
OLED_SCLK_Clr();
if(dat & 0x80)
{
OLED_SDIN_Set();
}
else
{
OLED_SDIN_Clr();
}
OLED_SCLK_Set();
dat <<= 1;
}
OLED_CS_Set();
OLED_DC_Set();
} /*
* setCol: SetLine:
* Set the column and line addresses
*********************************************************************************
*/
static void setPos(const int32 x, const int32 y)
{
sendData(0xb0 + y, OLED_CMD);
sendData(((x & 0xf0) >> 4) | 0x10, OLED_CMD);
sendData((x & 0x0f) | 0x02, OLED_CMD);
} /*
* lcd128x64update:
* Copy our software version to the real display
*********************************************************************************
*/
void lcd128x64update (void)
{
int32 x = 0, y = 0;
for(y = 0; y < (LCD_HEIGHT); y++)
{
setPos(0, y);
for(x = 0; x < LCD_WIDTH; x++)
{
sendData(frameBuffer[x][y], OLED_DATA);
}
}
}

视频数据制作

视频播放原理

视频播放其实和图片显示一样,只不过视频是由很多张连续的图像组成的,我们把这一张张图像按照一定速度一张一张依次显示出来就成了会动的视频。这一张张的图像叫做视频的帧。

图像数据提取

既然我们已经能显示一副图像了,而视频又是由连续的图像组成的,那用液晶播放视频是不是变得简单了很多。但问题是,我们要从哪里去得到这一张张的图像呢?而且大小必须与我们使用的液晶屏刚好合适。

这里就要用到KMPlayer这款软件了,KMPlayer是一个视频播放器,带有一个小工具,可以实现视频的”逐帧图”,意思就是把视频的每一帧都截图保存成一副图像。具体操作如下:

前缀指的是保存的图像的名字的前缀,这里把名字保存成连续的最大5位的数字,不足五位会在前面补0。这样做是为了方便后面再程序中处理。

图像数据转换

好了,现在我们得到我们想要的数据了,但是现在还不能直接播放,因为他是彩色的,而我们的液晶屏只支持黑白两种颜色,所以要进行转换,关于图像的二值化,网上有很多相关文章,二值化的质量直接影响到显示效果。我这里才用的是大律法求出图像的阈值。

阀值是图像二值化处理中非常重要的一个参数,意思就是:如果灰度图像的像素颜色值大于该阀值就把该点当作黑色,小于该阀值就把改点当作白色。最简单的做法就是把阀值取为127(255的一半),但是这种做法是不科学的,处理后的二值化图像效果也很不理想。关于阀值的选取是一门很深的学问,有很多经典的算法用于选取该阀值,理论我就不做过多描述了,感兴趣的自己趣网络上搜索相关资料,我这里采用的是比较经典的大律法(OTSU)。算法代码来自网络。

图像经过二值化之后,还需要经过最后一步,就是图像数据的提取,我们需要把这些图片中的像素点数据都提取出来,按照要显示的顺序排好,其实就是先把图像的每一个像素点提取出来按顺序排好,然后按顺序提取每一帧图像数据,最后把这些数据保存成二进制文件。使用的时候我们再把这个文件都入到内存中,这样这些数据在内存中就是按顺序排列的,我们只需要依次读出每一帧图像的数据显示出来即可。

如下图:

可能我描述的不是很清楚,大家看源码或许更好理解。

图像提取和转换源码(采用c++编写,用到了EasyBMP库):

#include <iostream>
#include <string>
#include <cmath>
#include <cstdlib>
#include <cstdio>
#include "EasyBMP/EasyBMP.h" /*
阀值是图像二值化处理中非常重要的一个参数,意思就是:如果灰度图像的像素
颜色值大于该阀值就把该点当作黑色,小于该阀值就把改点当作白色。最简单的
做法就是把阀值取为127(255的一半),但是这种做法是不科学的,处理后的二
值化图像效果也很不理想。关于阀值的选取是一门很深的学问,有很多经典的
算法用于选取该阀值,理论我就不做过多描述了,感兴趣的自己趣网络上搜索
相关资料,我这里采用的是比较经典的大律法(OTSU)。算法代码来自网络。
*/
int findThreshold(BMP frame) //大津法求阈值
{
#define GrayScale 256 //frame灰度级
int width = frame.TellWidth();
int height = frame.TellHeight();
int pixelCount[GrayScale] = {0};
float pixelPro[GrayScale] = {0};
int i, j, pixelSum = width * height, threshold = 0;
int r = 0 , g = 0 , b = 0 , data = 0;
//统计每个灰度级中像素的个数
for(i = 0; i < height; i++)
{
for(j = 0; j < width; j++)
{
r = frame(j, i)->Red;
g = frame(j, i)->Green;
b = frame(j, i)->Blue;
data = pow((pow(r, 2.2) * 0.2973 +
pow(g, 2.2) * 0.6274 +
pow(b, 2.2) * 0.0753), (1 / 2.2));
pixelCount[data]++;
}
}
//计算每个灰度级的像素数目占整幅图像的比例
for(i = 0; i < GrayScale; i++)
{
pixelPro[i] = (float)pixelCount[i] / pixelSum;
}
//遍历灰度级[0,255],寻找合适的threshold
float w0, w1, u0tmp, u1tmp, u0, u1, deltaTmp, deltaMax = 0;
for(i = 0; i < GrayScale; i++)
{
w0 = w1 = u0tmp = u1tmp = u0 = u1 = deltaTmp = 0;
for(j = 0; j < GrayScale; j++)
{
if(j <= i) //背景部分
{
w0 += pixelPro[j];
u0tmp += j * pixelPro[j];
}
else //前景部分
{
w1 += pixelPro[j];
u1tmp += j * pixelPro[j];
}
}
u0 = u0tmp / w0;
u1 = u1tmp / w1;
deltaTmp = (float)(w0 * w1 * pow((u0 – u1), 2)) ;
if(deltaTmp > deltaMax)
{
deltaMax = deltaTmp;
threshold = i;
}
}
return threshold;
} int main(int argc, char *argv[])
{
int nFrames;
BMP Input;
FILE *fp;
//unsigned int n = 0;
fp = fopen("output.bin", "wb"); /* 输出文件 */
//fp = fopen("output.h", "wb"); /* 输出文件 */
printf("******************************************************************\n");
printf("* 说 明 *\n");
printf("* *\n");
printf("* 本软件用于把24位色的BMP图像帧转换成用于LCD显示的二进制数 *\n");
printf("* 据文件。图片名必须为从00000开始的连续数字,总共5位,不足五位 *\n");
printf("* 要在前面用0补齐(也就是说本软件最多能处理99999张图片!)。如: *\n");
printf("* 00008.bmp。请把本软件和图片文件放在同一目录下! *\n");
printf("******************************************************************\n");
printf(">请输入图片数目: ");
scanf("%d", &nFrames);
printf("\n>转换开始……\n");
char FullPath[255], FileName[20];
for (int Frame = 0; Frame < nFrames; Frame++)
{
strcpy(FileName, "00000.bmp");
FileName[4] += (Frame / 1) % 10;
FileName[3] += (Frame / 10) % 10;
FileName[2] += (Frame / 100) % 10;
FileName[1] += (Frame / 1000) % 10;
FileName[0] += (Frame / 10000) % 10;
/*if(Frame > 9999)
{
strcpy(FileName, "000000.bmp");
FileName[5] += (Frame/1) % 10;
FileName[4] += (Frame/10) % 10;
FileName[3] += (Frame/100) % 10;
FileName[2] += (Frame/1000) % 10;
FileName[1] += (Frame/10000) % 10;
FileName[0] += (Frame/100000) % 10;
}*/
//fputs(FileName, fp);
//fputs("[ ] = \r\n{\r\n", fp);
printf("\r>正在处理: %s", FileName);
strcpy(FullPath, "");
strcat(FullPath, FileName);
if(Input.ReadFromFile(FullPath) == false)
{
printf("\n>打开图片文件出错!\n");
fclose(fp);
fp = NULL;
return –1;
}
int nSeg;
int threshold = findThreshold(Input);//阀值
nSeg = Input.TellHeight() / 8;
for (int iSeg = 0; iSeg < nSeg; iSeg++)
{
for (int x = 0; x < Input.TellWidth(); x++)
{
unsigned char Data;
//char Outdat[5] = {0};
Data = 0x00;
for (int j = 0; j < 8; j++)
{
int y = iSeg * 8 + j;
int r = Input(x, y)->Red;
int g = Input(x, y)->Green;
int b = Input(x, y)->Blue;
/*int brightness = (int)floor(
0.299*Input(x,y)->Red +
0.587*Input(x,y)->Green +
0.114*Input(x,y)->Blue);*/
int brightness = pow((pow(r, 2.2) * 0.2973 +
pow(g, 2.2) * 0.6274 +
pow(b, 2.2) * 0.0753), (1 / 2.2));
if (brightness > 255) brightness = 255;
if (brightness < 0) brightness = 0;
if (brightness > threshold) Data |= (0x01 << j);
}
//sprintf(Outdat,"0x%02X,",Data);
//fputs(Outdat, fp);
//fputs(&Data, fp);
fwrite(&Data, 1, 1, fp);
//n++;
//if(n >= 16)
//{
// n = 0;
// fputs("\r\n", fp);
//}
}
}
//fputs("\r\n};\r\n", fp);
}
printf("\n>转换结束……\n");
fclose(fp);
fp = NULL;
printf("\n>按任意键退出!\n");
getchar();
getchar();
return 0;
}

播放视频

图像数据提取并转换完成之后,就可以直接显示了。这相比上面的内容来说是很简单的,只不过是每次读出128个字节的数据并显示到液晶屏上,等待片刻之后再读取下一帧并显示。等待的时间是根据原视频计算出来的,如果原视频的帧率是24帧每秒,也就是每镇图像显示的时间是1/24秒。

接下来就是见证奇迹的时刻,这么小的屏幕居然也可以播放视频!

下面我直接给出视频显示的源码:

#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <wiringPi.h>
#include “lcd128x64.h”
#define N 128*8
int main(int argc, char *argv[])
{
int x = 0, y = 0;
lcd128x64setup();
int fd1;
unsigned char buf[N] = {0};
size_t nbyte = 0;
//while(1)
{
if(argc < 2)
{
printf("Please input Movie name,Like this: %s <movie.bin>\n", argv[0]);
return –1;
}
if((fd1 = open(argv[1], O_RDONLY)) < 0)
{
perror("Fail to open Movie file1!\n");
return –1;
}
printf("\nBegin to play movie!\n");
int nb = 0;
int n = 1;
while((nbyte = read(fd1, buf, N)) > 0)
{
//lcd128x64putbmpspeed(0, 0, 128, 64, buf, 0);
lcd128x64putbmpspeed(0, 0, 128, 64, buf, 1);
//lcd128x64update ();
printf("\rFrame %d", n++);
fflush(stdout);
delay(39);
}
printf("\nPlay movie Over!\n");
close(fd1);
}
printf("Exit!\n");
}

项目内文件截图

补充

暂时没有树莓派之OLED12864视频播放—BadApple

代码地址如下:
http://www.demodashi.com/demo/13218.html

注:本文著作权归作者,由demo大师代发,拒绝转载,转载需要作者授权

树莓派之OLED12864视频播放—BadApple的更多相关文章

  1. <<开源硬件创客 15个酷应用玩转树莓派>>

    本书共分18章,前3章是本书的基础章节,主要介绍了树莓派的一些基本情况和基本操作,来让读者了解树莓派的前世今生,掌握树莓派基本的使用方法.第4~18章主要介绍15个以树莓派为载体的酷炫应用,大家可以按 ...

  2. 树莓派播放视频的播放器omxplayer

    omxplyer为树莓派量身定做的一款GPU硬件加速的播放器,很好的解决了树莓派cpu计算力不足的缺点.(播放时cpu一定都不烫手) 1.安装方法: CTRL + ALT + T 调出终端命令行输入 ...

  3. StarRTC , AndroidThings , 树莓派小车,公网环境,视频遥控(一)准备工作

    原文地址:http://blog.starrtc.com/?p=48 啥也不说,先来个视频看看效果 视频播放器     00:00   00:54     概述为了体现StarRTC的实时音视频传输能 ...

  4. 树莓派-基于raspivid实现拍视频

    经过上一篇<<树莓派-安装摄像头模块>>之后 想要用摄像头模块拍一段视频的话,可以从命令行运行 raspivid 工具.下面这句命令会按照默认配置(长度5秒,分辨率1920x1 ...

  5. 树莓派搭建 Google TV

    出处:http://my.oschina.net/funnky/blog/142067 树莓派搭建 Google TV 目录:[ - ] Google TV是啥玩意 ? 搭建我们自己的Google T ...

  6. 树莓派USB存储设备自动挂载并通过脚本实现自动拷贝,自动播放视频,脚本自动升级等功能

    需求:首先需要树莓派自动挂载USB设备,然后扫描USB指定目录下文件,将相关文件拷贝至树莓派指定目录,然后通过omxplayer循环播放新拷贝文件视频 1. 树莓派实现USB存储设备自动挂载 树莓派U ...

  7. 详解树莓派Model B+控制蜂鸣器演奏乐曲

    步进电机以及无源蜂鸣器这些都需要脉冲信号才能够驱动,这里将用GPIO的PWM接口驱动无源蜂鸣器弹奏乐曲,本文基于树莓派Mode B+,其他版本树莓派实现时需参照相关资料进行修改! 1 预备知识 1.1 ...

  8. Python应用03 使用PyQT制作视频播放器

    作者:Vamei 出处:http://www.cnblogs.com/vamei 严禁任何形式转载. 最近研究了Python的两个GUI包,Tkinter和PyQT.这两个GUI包的底层分别是Tcl/ ...

  9. Linux主机上使用交叉编译移植u-boot到树莓派

    0环境 Linux主机OS:Ubuntu14.04 64位,运行在wmware workstation 10虚拟机 树莓派版本:raspberry pi 2 B型. 树莓派OS: Debian Jes ...

随机推荐

  1. [BZOJ2049][Sdoi2008]Cave 洞穴勘测 LCT模板

    2049: [Sdoi2008]Cave 洞穴勘测 Time Limit: 10 Sec  Memory Limit: 259 MBSubmit: 9705  Solved: 4674[Submit] ...

  2. SQL 建立多个字段唯一性校验

    由于在做压力测试,同一时间占用的问题. 两个用户同时下同一时间的订单,需要增加校验,第一个能保存的用户保存,第二个就不能让保存了. 问题是通过代码,怎么都做不到毫秒级校验,所以解决办法就只能是通过数据 ...

  3. 如何正确学习web前端流程以及如何找工作

    解释一下web前端工作是做啥的,Web前端开发工程师,主要职责是利用(X)HTML/CSS/JavaScript/Flash等各种Web技术进行客户端产品的开发.完成客户端程序(也就是浏览器端)的开发 ...

  4. HDU 1181.变形课-并查集

    变形课 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 131072/65536 K (Java/Others)Total Submis ...

  5. php中parse_url函数的源码及分析

    前言 看师傅们的文章时发现,parse_url出现的次数较多,单纯parse_url解析漏洞的考题也有很多,在此研究一下源码(太菜了看不懂,待日后再补充Orz) 源码 PHPAPI php_url * ...

  6. POJ 1321 棋盘问题【DFS/回溯/放与不放/类似n皇后】

    棋盘问题 Time Limit: 1000MS Memory Limit: 10000K Total Submissions: 62164 Accepted: 29754 Description 在一 ...

  7. MongoError: topology was destroyed解决方法

    MongoError: topology was destroyed 分析得出,出现这个问题是因为,当mongodb尝试写入某个数据的时候,连接被中断了! 解决方法:检查代码中是否存在操作数据的过程中 ...

  8. 2014 非常好用的开源 Android 测试工具

    http://www.php100.com/html/it/mobile/2014/1015/7495.html 当前有很大的趋势是转向移动应用平台,Android 是最广泛使用的移动操作系统,201 ...

  9. luogu P1418 选点问题

    题目描述 给出n个点,m条边,每个点能控制与其相连的所有的边,要求选出一些点,使得这些点能控制所有的边,并且点数最少.同时,任意一条边不能被两个点控制 输入输出格式 输入格式: 第一行给出两个正整数n ...

  10. 【Java】Java划水练习

    bzoj1000 A+B Problem Scanner sc=new Scanner(new BufferedInputStream(System.in)); 声明读入器 nextInt 读入整数 ...