一个用单片机放视频的小项目

总体设计思路

视频的播放实际上就是一系列的图片按照一定的顺序,以一定的时间间隔连续播放所产生的视觉效果。因此,使用单片机驱动LCD去播放视频实际上就是让单片机以一定的时间间隔向LCD的缓存推送图片,让其不断刷新屏幕去切换图片即可。在文章的最后我放入了这个项目的源工程文件供大家参考。

如果要同时播放音频那么就给单片机的管脚连入一个功放,单片机以音频电压不断给功放上电即可。由于单片机的频率只有1MHz,并且产生的也是方波信号,protues仿真效果不好,所以这里就没有加入声音的播放。

演示视频如下:

因此整个项目可以拆分成如下几部:

  • 将视频剪裁缩放到该显示器大小,并且对视频内容进行二值化
  • 将二值化后的视频流按照帧的顺序写入.h文件中方便读取
  • 在protues仿真平台上连接电路
  • 编写程序控制单片机不断向LCD写入图像。

1.处理视频

这里采用python进行视频的处理,使用了OpenCV对python支持的库对于视频进行操作,参考了一本关于OpenCV-python教程的书,我将该书的电子版放在这里,需要的同学可以自取。点击下载

这里将视频读入

源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import cv2
import math
import numpy
import csv

video=cv2.VideoCapture("1.flv")
writename="picture.csv"
file=open("1.txt",mode='w')
fourcc=cv2.VideoWriter_fourcc(*'XVID')
outvideo=cv2.VideoWriter("2.avi",fourcc,25.0,(128,64),False)
def trans(h,g,f,e,d,c,b,a): #将8位像素拼接成一个uint8
high_num=0
low_num=0
high_num=((((((high_num+a)<<1)+b)<<1)+c)<<1)+d
low_num=((((((low_num+e)<<1)+f)<<1)+g)<<1)+h
num=(high_num<<4)+low_num
return hex(num)
#不断循环对每一帧进行二值化,该帧的输出保存在程序同目录的"picture.csv"中,按住q可以不断快进,在需要截取帧暂停并复制"picture.csv"中的数据即可。
while (1):
ret,frame=video.read()
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
result=cv2.resize(gray,(128,64),interpolation=cv2.INTER_AREA)
ret,result=cv2.threshold(result,150,255,cv2.THRESH_BINARY)
print(result)
cv2.HoughLinesP
cv2.namedWindow("frame",cv2.WINDOW_NORMAL)
cv2.resizeWindow("frame",640,320)
cv2.imshow('frame',result)
outvideo.write(result)
cv2.waitKey(0)
for i in range(64):
for j in range(128):
if result[i,j]:
result[i,j]=0
else:
result[i,j]=1
bl=result.astype(numpy.int)
r0x=numpy.zeros((8,128),dtype=numpy.object)
for i in range(8):
for j in range(128):
r0x[i,j]=trans(bl[i*8,j],bl[i*8+1,j],bl[i*8+2,j],bl[i*8+3,j],bl[i*8+4,j],bl[i*8+5,j],bl[i*8+6,j],bl[i*8+7,j])
with open(writename,'w',newline='')as f:
csv_write=csv.writer(f)
for i in r0x:
csv_write.writerow(i)
if cv2.waitKey(1) & 0xFF == ord('q'):
break

video.release()
outvideo.release()
cv2.destroyAllWindows()

2.将帧数据写入.h文件

我们将上一步获取到的一些帧数据进行顺序排列并用','分隔即构成了我们需要显示视频的数据集。

接下了建立一个image.h文件,将所有数据粘贴到静态数组中:

.h文件的编写

这里要注意,我们采用的显示器分辨率为64 * 128共计8192个像素,每个像素的像素值为0/1,使用状态压缩将8个像素压缩到1B之后,每一帧占用的内存大小为8 * 128=1024B=1K。而protues仿真软件中,RAM最大为64K,考虑到还需要一部分空间来存放其他代码,所以这里最多可以存放62~63帧,视其他代码占用空间决定。

3.在protues上连接电路

选取的单片机为经典的AT89C51,显示器型号为AMPIPR128*64

这里连线采用标号的方式,方便连接和查看:

LCD部分
单片机部分

有关单片机管脚的含义和使用方法以及该LCD各个引脚的意义大家可以自行搜索学习,网上有很多相关的资料。

4.编写单片机的程序

要编写单片机驱动LCD的程序,我们需要了解该LCD各个引脚的作用和如何驱动它显示图像,同上,网上有很多相关的手册供大家自行查询。那么这里我们直接使用一个已经封装好的驱动文件对该显示器进行底层操作,我们只关注应用层面的逻辑即可。

源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <lcd.h>
#include <image.h>
#define uchar unsigned char
sbit power=P2^0;
sbit pause=P2^1;
sbit clear=P2^1;
bit is_off=1;
bit is_pause=1;
bit is_clear=0;
void delay(int a)
{
while (a--);
}
void main()
{
uchar i,j,k;
for (i=0;i<63;i++)
{
vShowGraph(0,0,128,8,image[i]);
delay(5000);
if (i==20)
{
j=3;
while (j--)
{
for (k=i;k<25;k++)
{
vShowGraph(0,0,128,8,image[k]);
delay(5000);
}
}
}
if (i==41)
{
j=3;
while (j--)
{
for (k=i;k<45;k++)
{
vShowGraph(0,0,128,8,image[k]);
delay(5000);
}
}
}
}
}

封装好的显示驱动.h文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include <at89x51.h>
#define RST P1_2 /*复位信号,低电平复位*/
#define E P1_5 /*E 使能信号,为H或1时表示DRAM数据读到DB7~DB0,为L或0表示锁存DB7~DB0*/
#define RW P1_4 /* RW 为1 E 为1时数据被读到DB7~DB0,为0 且E为下降沿时DB7~DB0的数据被写入到IR(指令寄存器)或DR(数据寄存器)*/
#define RS P1_3 /* DI为1表示DB7~DB0为显示数据,为0表示DB7~DB0为显示指令数据*/
#define CS1 P1_0 /*CS1为1表示选择该芯片(右半屏)信号*/
#define CS2 P1_1 /*CS2为1表示选择该芯片(左半屏)信号*/
#define LCDPORT P3
#define BUSYSTATUS P3_7 //忙状态位。//
#define DISONSTATUS P3_5 //显示开关状态位。//
#define RSTSTATUS P3_4 //复位状态位。
#define LCDSTARTROW 0xC0 //设置起始行指令。
#define LCDPAGE 0xB8 //设置页指令。
#define LCDLINE 0x40 //设置列指令。

bit bCheckBusy()
{
LCDPORT=0x00;
RW=1;
RS=0;
E=1;
E=0;
return BUSYSTATUS;
}

void vWriteData(unsigned char ucData)
{
while(bCheckBusy());
LCDPORT=0xFF;
RW=0;
RS=1;
LCDPORT=ucData;
E=1;
E=0;
}

void vWriteCMD(unsigned char ucCMD)
{
while(bCheckBusy());
LCDPORT=0xFF;
RW=0;
RS=0;
LCDPORT=ucCMD;
E=1;
E=0;
}

void vLCDInitialize()
{
CS1=1;
CS2=1;
vWriteCMD(0x38); //8位形式,两行字符。
vWriteCMD(0x0F); //开显示。
vWriteCMD(0x01); //清屏。
vWriteCMD(0x06); //画面不动,光标右移。
vWriteCMD(LCDSTARTROW); //设置起始行。
}

void vShowCustomRow(unsigned char ucPage,unsigned char ucLine,unsigned char ucWidth,unsigned char *ucaRow)
{
unsigned char ucCount; //取值范围:ucPage:0~7;ucLine:0~127;
if(ucLine<64) //ucWidth:0~127;ucLine+ucWidth<1128。
{
CS1=0;
CS2=1;
vWriteCMD(LCDPAGE+ucPage);
vWriteCMD(LCDLINE+ucLine);
if((ucLine+ucWidth)<64)
{
for(ucCount=0;ucCount<ucWidth;ucCount++)
vWriteData(*(ucaRow+ucCount));
}
else
{
for(ucCount=0;ucCount<64-ucLine;ucCount++)
vWriteData(*(ucaRow+ucCount));
CS1=1;
CS2=0;
vWriteCMD(LCDPAGE+ucPage);
vWriteCMD(LCDLINE);
for(ucCount=64-ucLine;ucCount<ucWidth;ucCount++)
vWriteData(*(ucaRow+ucCount));
}
}
else
{
CS1=1;
CS2=0;
vWriteCMD(LCDPAGE+ucPage);
vWriteCMD(LCDLINE+ucLine-64);
for(ucCount=0;ucCount<ucWidth;ucCount++)
vWriteData(*(ucaRow+ucCount));
}
}

void vShowOneChin(unsigned char ucPage,unsigned char ucLine,unsigned char *ucaChinMap)
{
vShowCustomRow(ucPage,ucLine,16,ucaChinMap);
vShowCustomRow(ucPage+1,ucLine,16,ucaChinMap+16);
}

void vShowOneChar(unsigned char ucPage,unsigned char ucLine,unsigned char *ucaCharMap)
{
vShowCustomRow(ucPage,ucLine,8,ucaCharMap);
vShowCustomRow(ucPage+1,ucLine,8,ucaCharMap+8);
}

void vShowGraph(unsigned char ucPage,unsigned char ucLine,unsigned char ucWidth,unsigned char ucHigh,unsigned char * ucaGraph)
{
unsigned char ucCount;
for(ucCount=0;ucCount<ucHigh;ucCount++)
vShowCustomRow(ucPage+ucCount,ucLine,ucWidth,ucaGraph+ucCount*ucWidth);
}

5.结束语(附工程下载链接)

该项目是博主大三时做的一个电子线路设计,给我最大的启发就是我们在生活中看起来复杂的项目,实际上其背后的原理是简单的,就如视频的播放一样。但同时,简单的事情是复杂的,之后在做语音识别的时候,采用自己提取的语音特征去分辨数字0-9,整体的识别率不到30%。因此,判断一件事难易程度不能凭空去想,而是要思考其运行机制和模块构成,分析其实现原理从而对于每个项目有清晰的认知。

工程文件下载:点击下载