目录
1 任务介绍
2 硬件设备
3 脚本工具
4 软件SPI实现W25Q64数据存储与读取
5 硬件IIC实现OLED显示
6 主函数调用
7 实现效果
1 任务介绍
学完江科协的IIC通信和SPI通信后想趁热打铁实现一个小项目。该项目可以通过OLED实现视频播放(视频无声)。基本思路为通过IIC通信实现OLED显示,通过SPI通信实现数据存储与读取。单靠ARM本身的存储空间是没法存下一个视频的数据,因此需要将视频数据存储在外挂芯片W25Q64中。
不用担心视频图像取模问题,我这里写了一个脚本可以实现视频转128*64(0.96寸OLED)的文本数据。
2 硬件设备
STM32F103C8T6最小系统板、0.96寸OLED、W25Q64(可以直接用江科协提供的材料)
3 脚本工具
视频本质上是播放一帧帧图片,因此在存储视频时需要将视频转换成图片进行存储,且0.96寸OLED的显示像素为128*64,所以对应的图片也必须改成这种尺寸。另外还需要将彩色视频转化为二值图片才能显示。
视频的数据量比较多,这里我直接写了一个python脚本实现整个视频取模,最终视频其实就变成一个二维数组picture[count][pixs],其中count是视频帧数,pixs为视频像素,0.96寸的OLED其像素固定为128*64,一个数据是8bit,所以pixs=128*8. 运行下面这段代码会在工程目录中多一个array.txt文件,然后将这个文件的数据依次写入W25Q64中(依次最多写入20张图片,如果帧率较高的话要分批写入)。
import os
import cv2
orig_video_path = r'C:\Users\Lenovo\Desktop\Myroject\getpicture\gege.avi'# 原视频路径
save_pic = r'C:\Users\Lenovo\Desktop\Myroject\getpicture\picture\\'
def video():
videoCapture = cv2.VideoCapture(orig_video_path)
f = int(videoCapture.get(cv2.CAP_PROP_FPS))
print('原视频帧率为:'+str(f))
fps = 40 # 保存视频的帧率,可改变
size = (128, 64) # 保存视频大小,必须和OLED分辨率相对应
save_dir = orig_video_path[:-4]+'_new.avi'
videoWriter = cv2.VideoWriter(save_dir,
cv2.VideoWriter_fourcc('D', 'I', 'V', 'X'), fps, size)
while True:
success, frame = videoCapture.read()
if success:
img = cv2.resize(frame, size)
videoWriter.write(img)
else:
print('break')
break
# 释放对象,不然可能无法在外部打开
videoWriter.release()
# 视频转彩色图片
def video_pic():
video_path = orig_video_path[:-4]+'_new.avi'
cap = cv2.VideoCapture(video_path)
sucess = cap.isOpened()
frame_count = 0
i = 0
while sucess:
frame_count += 1
sucess, frame = cap.read()
if(frame is None):
break
if (frame_count % 5 == 0): # 每隔5帧保存一张图片(每帧都保存数据太多了)
i += 1
cv2.imwrite(save_pic+'\\'+str(i)+'.jpg', frame)
cap.release()
#彩色图片转二值图片并保存为txt文件
def pic_txt():
file = open('array.txt', 'w')
pictures = 56 # 需要转换的图片总数量
for p in range(len(os.listdir(save_pic))):
nums = []
file_path = save_pic+str(p)+'.jpg'
img = cv2.imread(file_path, 0)
img[img>140] = 255 # 阈值设置为140,即像素大于140的为0,小于140的为1
img[img<=140] = 1
img[img==255] = 0
# 阳码
# img[img<=140] = 0
# img[img==255] = 1
row, col = img.shape
for i in range(0, row,8):
for j in range(0,col,1):
num = 0
for k in range(8):
num += img[i+k][j]*pow(2,k) # 不能直接加,要乘以2的n次方
nums.append(num)
line = '{'
for i in range(len(nums)):
if(i%32 == 0):
file.write(line+'\n')
print(line)
line = ''
line += str(hex(nums[i]))+','
line = line[:-1]+'\n'
file.write(line)
line = '}, /***** ' + str(p) + ' *****/\n'
file.write(line)
file.close()
if __name__ == '__main__':
video() # 先转换视频尺寸
video_pic() # 将视频转为图片进行保存(当然也可以不保存,这里只是方便分析)
pic_txt() # 图片转文本,用于OLED水平地址模式显示
当然要是不嫌麻烦也可以用PCtoLCD2002这个软件对图片手动取模,但是这个软件每次只能转一张图片,而且只能转二值bmp格式图片,如果是彩色图片还需要用电脑自带的画图工具修改图片格式,所以不建议使用软件转,如果你非要用软件取字模就按照下面的格式进行设置:
4 软件SPI实现W25Q64数据存储与读取
如果完成上述步骤,你就可以获得一个array.txt文本文档,这个文本文档里面存放着所有图片数据,把数据定义成二维数组,将这个二维数据一次写入到W25Q64。我一共写了32张图片,每张图片的像素为128*64bit,即128*8byte,所以数组维度为[32][128*8]。SPI实现多张图片的写入函数如下:
W25Q64.c文件,其中
void W25Q64_PageProgram_N(uint32_t Address, uint8_t DataArray[][128*8], uint16_t Count)
函数用于多张图片的写入。Address为写入初始地址,DataArray为图片数组,Count为图片数量。
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
用于读取W25Q64数据,Address为读取数据块的初始地址,DataArray为读取数据缓存区,Count为数据总长度,单位为byte
#include "stm32f10x.h" // Device header
#include "MySPI.h"
#include "W25Q64.h"
#include "W25Q64_Ins.h"
void W25Q64_Init(void)
{
MySPI_Init();
}
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{
MySPI_Start(); //开始通信
MySPI_SwapByte(W25Q64_JEDEC_ID);//发送获取设备ID命令
*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//接收设备厂商号
*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//接收设备高8位ID
*DID <<= 8;
*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);//接收设备低8位ID
MySPI_Stop();//停止通信
}
void W25Q64_WriteEnable(void)
{
MySPI_Start();
MySPI_SwapByte(W25Q64_WRITE_ENABLE);//发送写指令(SPI固定,所有起始指令后面接的都是控制指令)
MySPI_Stop();
}
void W25Q64_WaitBusy(void) //读状态寄存器,等待读写就绪态
{
uint32_t Timeout;
MySPI_Start();
MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
Timeout = 1000000;
while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)
{
Timeout --;
if (Timeout == 0) //等待超时退出
{
break;
}
}
MySPI_Stop();
}
/*
function 向W25Q46写入存储数据,用于写入一张图
@param Address 写入地址
@param DataArray 写入数据
@param Count 一张图的数据长度一般为128*8
*/
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{
uint16_t i,j=0;
uint16_t L = Count / 256; //一次数据传输最多256字节,第二次要重新写入地址
uint16_t M = Count % 256;
uint32_t Address1 = Address;
//开始传输数据
while(L > j)
{
W25Q64_WriteEnable();
Address1 = (uint32_t)(Address + (256*j));
MySPI_Start();
MySPI_SwapByte(W25Q64_PAGE_PROGRAM);
MySPI_SwapByte(Address1 >> 16);//每次发8字节,但是地址一共24字节,所以发三次,先发送高8位
MySPI_SwapByte(Address1 >> 8);
MySPI_SwapByte(Address1);
for (i = 0; i < 256; i ++)
{
MySPI_SwapByte(DataArray[i + 256*j]);
}
MySPI_Stop();
W25Q64_WaitBusy();
j++;
}
W25Q64_WriteEnable();
Address1 = (uint32_t)(Address + (256*j));
MySPI_Start();
MySPI_SwapByte(W25Q64_PAGE_PROGRAM);
MySPI_SwapByte(Address1 >> 16);//每次发8字节,但是地址一共24字节,所以发三次,先发送高8位
MySPI_SwapByte(Address1 >> 8);
MySPI_SwapByte(Address1);
for (i = 0; i < M; i ++)
{
MySPI_SwapByte(DataArray[i + 256*j]);
}
MySPI_Stop();
W25Q64_WaitBusy();
}
/*
function 向W25Q46写入存储数据,用于写入一张图
@param Address 写入地址
@param DataArray 写入数据
@param Count 图片数量,图片默认像素为128*64
*/
void W25Q64_PageProgram_N(uint32_t Address, uint8_t DataArray[][128*8], uint16_t Count)
{
int i;
uint32_t temp_address = Address;
//一张图1KB,一个扇区最多存放4张图,每次最多擦除一个扇区
uint16_t pages = Count / 4;
pages += Count % 4;
//先擦除扇区
for(i = 0; i<pages; i++)
{
W25Q64_SectorErase(temp_address);
uint32_t temp1_address = temp_address;
for(int j=0; j<4; j++)
{
W25Q64_PageProgram(temp1_address, DataArray[i*4+j], 128*8);
temp1_address += 0x000400; //每写入一张图片,向前移动1024byte
}
temp_address += 0x001000;
}
}
/*
function 擦除存储器数据
@param Address 需要擦除的扇区地址,一个扇区
*/
void W25Q64_SectorErase(uint32_t Address)
{
W25Q64_WriteEnable();
MySPI_Start();
MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB); //发送擦除指令,每次擦除一个扇区
MySPI_SwapByte(Address >> 16); //擦除地址
MySPI_SwapByte(Address >> 8);
MySPI_SwapByte(Address);
MySPI_Stop();
W25Q64_WaitBusy();
}
/*
function 从W25Q46发送读取数据
@param Address 读取起始地址地址
@param DataArray 数据缓冲
@param Count 读取数据长度
*/
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{
uint32_t i;
MySPI_Start();
MySPI_SwapByte(W25Q64_READ_DATA);
MySPI_SwapByte(Address >> 16);
MySPI_SwapByte(Address >> 8);
MySPI_SwapByte(Address);
for (i = 0; i < Count; i ++)
{
DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
}
MySPI_Stop();
}
5 硬件IIC实现OLED写入
#include "OLED_IIC.h"
void My_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
I2C_InitTypeDef I2C_InitStructure;
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_ClockSpeed = 400000;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStructure.I2C_OwnAddress1 = 0x30;
I2C_Init(I2C2, &I2C_InitStructure);
I2C_Cmd(I2C2, ENABLE);
}
//发送一个字节数据
void My_OLED_WriteData(uint16_t addr, uint16_t data)
{
while(I2C_GetFlagStatus(I2C2,I2C_FLAG_BUSY));
I2C_GenerateSTART(I2C2,ENABLE);
while(!I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C2,0X78,I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
I2C_SendData(I2C2, addr);
while(!I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING));
//I2C_EVENT_MASTER_BYTE_TRANSMITTING
I2C_SendData(I2C2, data);
while(!I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING));
I2C_GenerateSTOP(I2C2,ENABLE);
}
//写命令
void My_WriteCmd(unsigned char cmoodcmd)
{
My_OLED_WriteData(0x00,cmoodcmd);
}
//写数据
void My_WriteDATA(unsigned char ic2data)
{
My_OLED_WriteData(0x40,ic2data);
}
//设置光标
void My_Starting_point(unsigned char x,unsigned char y)
{
My_WriteCmd(0xb0+y);
My_WriteCmd((x&0xf0)>>4|0x10); //1111 0000 ->0000 1111|0X10=0001 1111
My_WriteCmd((x&0x0f)|0x01);//0000 1111 | 0000 0001 =0000 1111
}
void oled_fill(unsigned char Flii_data)
{
uint8_t i, j;
for (j = 0; j < 8; j++)
{
My_Starting_point(j, 0);
for(i = 0; i < 128; i++)
{
My_WriteDATA(0x00);
}
}
}
//清屏
void My_OLED_Clear(void)
{
oled_fill(0x00);
}
void My_OLED_Init()
{
uint32_t i, j;
for (i = 0; i < 1000; i++) //上电延时
{
for (j = 0; j < 1000; j++);
}
My_Init(); //端口初始化
My_WriteCmd(0xAE); //关闭显示
My_WriteCmd(0xD5); //设置显示时钟分频比/振荡器频率
My_WriteCmd(0x80);
My_WriteCmd(0xA8); //设置多路复用率
My_WriteCmd(0x3F);
My_WriteCmd(0xD3); //设置显示偏移
My_WriteCmd(0x00);
My_WriteCmd(0x40); //设置显示开始行
My_WriteCmd(0xA1); //设置左右方向,0xA1正常 0xA0左右反置
My_WriteCmd(0xC8); //设置上下方向,0xC8正常 0xC0上下反置
My_WriteCmd(0xDA); //设置COM引脚硬件配置
My_WriteCmd(0x12);
My_WriteCmd(0x81); //设置对比度控制
My_WriteCmd(0xCF);
My_WriteCmd(0xD9); //设置预充电周期
My_WriteCmd(0xF1);
My_WriteCmd(0xDB); //设置VCOMH取消选择级别
My_WriteCmd(0x30);
My_WriteCmd(0xA4); //设置整个显示打开/关闭
My_WriteCmd(0xA6); //设置正常/倒转显示
My_WriteCmd(0x20); //修改地址模式为水平模式
My_WriteCmd(0x00);
My_WriteCmd(0x8D); //设置充电泵
My_WriteCmd(0x14);
My_WriteCmd(0xAF); //开启显示
My_WriteCmd(0x2E); //关闭滚动显示
My_OLED_Clear(); //OLED清屏
}
6 主函数
uint8_t ArrayRead[128*8] = {0};
//void begin_show(void);
void twinkle_show(uint8_t pic[], uint16_t length, uint16_t width);
int main(void)
{
// OLED_Init();
My_OLED_Init();
W25Q64_Init();
// W25Q64_PageProgram_N(0x000000, OLED_PIC2, 20); //SPI写图片数据
uint32_t temp_address = 0x000000;
int i =0,j=0 ;
for(i=0; i<32; i++)
{
My_OLED_Clear();
My_Starting_point(0, 0);
// OLED_Clear();
// OLED_SetCursor(0, 0);
W25Q64_ReadData(temp_address, ArrayRead, 128*8); //每次读一张图片
// twinkle_show(ArrayRead, 128, 64);
for(j=0; j<128 *8; j++)
{
My_WriteDATA(ArrayRead[j]); //硬件IIC写OLED
// OLED_WriteData(ArrayRead[j]);//软件IIC写OLED
}
Delay_ms(20);
temp_address += 0x000400;
}
while (1)
{
}
}
7 实现效果
视频没法上传,这里实现效果直接贴出b站UP主的效果,如有侵权请及时联系。