STM32使用W25Q64模块在LCD显示视频
摘要:本项目基于STM32F103C8T6微控制器,结合W25Q64外部Flash存储模块和ST7789 LCD显示屏,实现了视频播放功能。通过FreeRTOS实时操作系统创建视频播放任务,利用Python脚本将视频转换为240×240 RGB565格式的.bin文件,再通过串口传输至STM32并存储到外部Flash。系统采用双SPI接口分别控制LCD显示和Flash读写,实现了从视频解码、数据传输到屏幕显示的完整流程,重点解决了Flash数据写入的地址管理和扇区擦除问题,同时熟悉了8Pin LCD屏幕的使用方法。
一、设备准备
1.元器件:
为了简便,本次小实验使用 STM32F103c8t6、W25Q64模块作外部Flash、(240*240)
ST7789屏幕、串口、ST-Link、面包板、(可作为测试的LED灯)。
2.接线:

(1)SPI引脚:
采用 SPI1,连接 c8t9 与 LCD 屏幕之间的数据传输。
采用 SPI2,连接 W25Q64 模块与 c8t6 ,用于数据写入
Cube MX配置如下:二者配置一致

(2)USART引脚:
配置 USART1 为异步串口,用于视频数据通过串口写入W25Q64模块。
可以调整波特率大小,减小将视频数据写入的时间。但是我实际发现当波特率设置为921600 时,总是会写入错误,而115200可以正常写入。
Cube MX配置如下:

(3)GPIO引脚:
PA1设置为LED任务,用来判断验证程序是否正常运行。
其余 GPIO 口可查看ST7789模块和W25Q64模块的手册,按需接,并且贴上 Label 。
Cube MX配置如下:

二、程序设计
1.Cube MX 配置 Free RTOS 的CMSIS V2
(1)堆栈修改:
在 config parameters 中将 TOTAL HEAP SIZE 从默认的 3000 多修改为 8192 ,不然由于太多Tasks,会导致堆栈溢出,任务创建失败。

(2)任务创建:
自行创建任务VideoTask,(忽略FlashTask、DisPlayTask、UartTask)。

2.具体代码逻辑实现:
(1)导入.c\.h文件到keil文件
包括:lcd_st7789.c,lcd_st7789.h,w25q64.h,w25q64.c
/* lcd_st7789.h */
#pragma once
#include "main.h"
extern SPI_HandleTypeDef hspi1;
void ST7789_Init(void);
void ST7789_SetWindow(uint16_t x0,uint16_t y0,uint16_t x1,uint16_t y1);
HAL_StatusTypeDef ST7789_WritePixels(const uint8_t *data,uint32_t len);
void ST7789_Clear(uint16_t color);
/*lcd_st7789.c*/
#include "lcd_st7789.h"
#include "main.h"
#include "spi.h"
#define CS_L() HAL_GPIO_WritePin(LCD_CS_GPIO_Port,LCD_CS_Pin,GPIO_PIN_RESET)
#define CS_H() HAL_GPIO_WritePin(LCD_CS_GPIO_Port,LCD_CS_Pin,GPIO_PIN_SET)
/* LCDд ָ */
static void cmd(uint8_t c)
{
HAL_GPIO_WritePin(LCD_DC_GPIO_Port,LCD_DC_Pin,GPIO_PIN_RESET);
CS_L();
HAL_SPI_Transmit(&hspi1,&c,1,100);
CS_H();
}
/* n ʾ */
static void data(const uint8_t *p,uint16_t n)
{
HAL_GPIO_WritePin(LCD_DC_GPIO_Port,LCD_DC_Pin,GPIO_PIN_SET);
CS_L();
HAL_SPI_Transmit(&hspi1,(uint8_t*)p,n,100);
CS_H();
}
/* Ļ ʼ */
void ST7789_Init(void)
{
uint8_t d[16];
HAL_GPIO_WritePin(LCD_BLK_GPIO_Port,LCD_BLK_Pin,GPIO_PIN_SET);
HAL_GPIO_WritePin(LCD_RES_GPIO_Port,LCD_RES_Pin,GPIO_PIN_RESET);
HAL_Delay(20);
HAL_GPIO_WritePin(LCD_RES_GPIO_Port,LCD_RES_Pin,GPIO_PIN_SET);
HAL_Delay(120);cmd(0x11);HAL_Delay(120);cmd(0x3A);d[0]=0x55;data(d,1);
cmd(0x36);
d[0]=0x00;
data(d,1);
cmd(0x21);
cmd(0x13);
cmd(0x29);
HAL_Delay(20);
}
/* ʾ */
void ST7789_SetWindow(uint16_t x0,uint16_t y0,uint16_t x1,uint16_t y1)
{
uint8_t d[4];
cmd(0x2A);
d[0]=x0>>8;
d[1]=x0;
d[2]=x1>>8;
d[3]=x1;
data(d,4);
cmd(0x2B);
d[0]=y0>>8;
d[1]=y0;
d[2]=y1>>8;
d[3]=y1;
data(d,4);
cmd(0x2C);
}
/* ʾ*/
HAL_StatusTypeDef ST7789_WritePixels(const uint8_t *p,uint32_t n)
{
HAL_GPIO_WritePin(LCD_DC_GPIO_Port,LCD_DC_Pin,GPIO_PIN_SET);
CS_L();
HAL_StatusTypeDef s=HAL_SPI_Transmit(&hspi1,(uint8_t*)p,n,1000);
CS_H();
return s;
}
/* */
// ST7789_Clear(0x0000);白色
// ST7789_Clear(0xFFFF);黑色
void ST7789_Clear(uint16_t color)
{
static uint8_t line[240*2];
for(uint16_t i=0;i<240;i++)
{
line[i*2] = color >> 8;
line[i*2+1] = color & 0xFF;
}
ST7789_SetWindow(0, 0, 239, 239);
for(uint32_t y=0; y < 240; y++)
{
ST7789_WritePixels(line,sizeof(line));
}
}
/*w25q64.c*/
#include "w25q64.h"
#define W25_CS_GPIO_Port GPIOB
#define W25_CS_Pin GPIO_PIN_12
#define W25_CS_L() HAL_GPIO_WritePin(W25_CS_GPIO_Port,W25_CS_Pin,GPIO_PIN_RESET)
#define W25_CS_H() HAL_GPIO_WritePin(W25_CS_GPIO_Port,W25_CS_Pin,GPIO_PIN_SET)
#define CMD_RDID 0x9Fu
#define CMD_READ 0x03u
#define CMD_WREN 0x06u
#define CMD_PP 0x02u
#define CMD_RDSR1 0x05u
#define CMD_SECTOR_ERASE 0x20u
static osMutexId_t bus_mutex;
static HAL_StatusTypeDef xfer(uint8_t v,uint8_t *r){return HAL_SPI_TransmitReceive(&hspi2,&v,r,1,100);}
static HAL_StatusTypeDef wait_ready(uint32_t timeout_ms){uint8_t c=CMD_RDSR1,s=0; uint32_t start=HAL_GetTick(); do {W25_CS_L(); if(HAL_SPI_Transmit(&hspi2,&c,1,100)!=HAL_OK){W25_CS_H(); return HAL_ERROR;} if(xfer(0xff,&s)!=HAL_OK){W25_CS_H(); return HAL_ERROR;} W25_CS_H(); if((HAL_GetTick()-start)>=timeout_ms)return HAL_TIMEOUT;} while(s&1u); return HAL_OK;}
static void write_enable(void){uint8_t c=CMD_WREN; W25_CS_L(); HAL_SPI_Transmit(&hspi2,&c,1,100); W25_CS_H();}
uint32_t W25Q64_ReadJedecId(void){uint8_t c=CMD_RDID,b[3]; W25_CS_L(); HAL_SPI_Transmit(&hspi2,&c,1,100); HAL_SPI_Receive(&hspi2,b,3,100); W25_CS_H(); return ((uint32_t)b[0]<<16)|((uint32_t)b[1]<<8)|b[2];}
HAL_StatusTypeDef W25Q64_Read(uint32_t addr,uint8_t *buf,uint32_t len){uint8_t c[4]={CMD_READ,(uint8_t)(addr>>16),(uint8_t)(addr>>8),(uint8_t)addr}; if(bus_mutex)osMutexAcquire(bus_mutex,osWaitForever); W25_CS_L(); HAL_StatusTypeDef s=HAL_SPI_Transmit(&hspi2,c,4,100); if(s==HAL_OK)s=HAL_SPI_Receive(&hspi2,buf,len,1000); W25_CS_H(); if(bus_mutex)osMutexRelease(bus_mutex); return s;}
HAL_StatusTypeDef W25Q64_PageProgram(uint32_t addr,const uint8_t *buf,uint16_t len){if(!len||len>W25Q64_PAGE_SIZE||(addr&0xffu)+len>W25Q64_PAGE_SIZE)return HAL_ERROR; if(bus_mutex)osMutexAcquire(bus_mutex,osWaitForever); write_enable(); uint8_t c[4]={CMD_PP,(uint8_t)(addr>>16),(uint8_t)(addr>>8),(uint8_t)addr}; W25_CS_L(); HAL_StatusTypeDef s=HAL_SPI_Transmit(&hspi2,c,4,100); if(s==HAL_OK)s=HAL_SPI_Transmit(&hspi2,(uint8_t*)buf,len,100); W25_CS_H(); if(s==HAL_OK)s=wait_ready(1000); if(bus_mutex)osMutexRelease(bus_mutex); return s;}
HAL_StatusTypeDef W25Q64_SectorErase(uint32_t addr){uint8_t c[4]={CMD_SECTOR_ERASE,(uint8_t)(addr>>16),(uint8_t)(addr>>8),(uint8_t)addr}; if(bus_mutex)osMutexAcquire(bus_mutex,osWaitForever); write_enable(); W25_CS_L(); HAL_StatusTypeDef s=HAL_SPI_Transmit(&hspi2,c,sizeof(c),100); W25_CS_H(); if(s==HAL_OK)s=wait_ready(3000); if(bus_mutex)osMutexRelease(bus_mutex); return s;}
void W25Q64_SetBusMutex(osMutexId_t m){bus_mutex=m;}
/*w25q64.h*/
#pragma once
#include "main.h"
#include "cmsis_os2.h"
extern SPI_HandleTypeDef hspi2;
#define W25Q64_PAGE_SIZE 256u
#define W25Q64_SECTOR_SIZE 4096u
HAL_StatusTypeDef W25Q64_Read(uint32_t addr, uint8_t *buf, uint32_t len);
HAL_StatusTypeDef W25Q64_PageProgram(uint32_t addr, const uint8_t *buf, uint16_t len);
HAL_StatusTypeDef W25Q64_SectorErase(uint32_t addr);
uint32_t W25Q64_ReadJedecId(void);
(2)任务Task实现:
在 “ freertos.c ” 文件中编写任务函数即可。
void StartVideoTask(void *argument)
{
uint8_t line[480];
const uint32_t video_base = 0x001000u;
const uint32_t frame_bytes = 115200u;
const uint32_t frame_count = 72u;
const uint32_t frame_period = 0u;
/*
frame_period 控制时间额外延长
5 FPS -> 200 ms
10 FPS -> 100 ms
15 FPS -> 66 ms
*/
uint32_t frame = 0u;
(void)argument;
ST7789_Init();
for (;;)
{
uint32_t frame_base = video_base + frame * frame_bytes;
ST7789_SetWindow(0u, 0u, 239u, 239u);
for (uint16_t y = 0u; y < 240u; y++)
{
if (W25Q64_Read(frame_base + (uint32_t)y * sizeof(line),
line, sizeof(line)) != HAL_OK)
{
memset(line, 0, sizeof(line));
}
if (ST7789_WritePixels(line, sizeof(line)) != HAL_OK)
{
break;
}
}
frame++;
if (frame >= frame_count)
{
frame = 0u;
}
osDelay(frame_period);
}
}
(3)将目标视频转换为 240*240 格式的 RGB565 的 .bin 格式文件
以下是一个使用 GPT 生成的python格式代码,如果需要不同格式,可以自行修改其中参数,得到需要的.bin格式文件。
但是在运行之前,需要先安装 ffmpeg 软件,浏览器下载就行。
#!/usr/bin/env python3
"""Convert a video to consecutive 240x240 RGB565 little-endian frames."""
import argparse
import struct
import subprocess
import sys
WIDTH = 240
HEIGHT = 240
FRAME_BYTES = WIDTH * HEIGHT * 2
def convert(input_path, output_path, fps, max_frames, ffmpeg_path):
vf = (
f"fps={fps},scale={WIDTH}:{HEIGHT}:force_original_aspect_ratio=decrease,"
f"pad={WIDTH}:{HEIGHT}:(ow-iw)/2:(oh-ih)/2:black,format=rgb24"
)
command = [
ffmpeg_path, "-hide_banner", "-loglevel", "error", "-i", input_path,
"-vf", vf, *( ["-frames:v", str(max_frames)] if max_frames > 0 else [] ), "-f", "rawvideo", "-pix_fmt", "rgb24", "-"
]
process = subprocess.Popen(command, stdout=subprocess.PIPE)
frames = 0
try:
with open(output_path, "wb") as output:
while max_frames <= 0 or frames < max_frames:
raw = process.stdout.read(WIDTH * HEIGHT * 3)
if not raw:
break
if len(raw) != WIDTH * HEIGHT * 3:
raise RuntimeError("ffmpeg returned an incomplete frame")
packed = bytearray(FRAME_BYTES)
for pixel in range(WIDTH * HEIGHT):
r, g, b = raw[pixel * 3:pixel * 3 + 3]
value = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)
struct.pack_into("<H", packed, pixel * 2, value)
output.write(packed)
frames += 1
finally:
process.stdout.close()
return_code = process.wait()
if return_code != 0:
raise RuntimeError(f"ffmpeg failed with exit code {return_code}")
print(f"frames={frames} bytes={frames * FRAME_BYTES} output={output_path}")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("input", help="input video accepted by ffmpeg")
parser.add_argument("output", help="output .bin file")
parser.add_argument("--fps", type=float, default=5.0)
parser.add_argument("--max-frames", type=int, default=72)
parser.add_argument("--ffmpeg", default="ffmpeg", help="path to ffmpeg.exe")
args = parser.parse_args()
if args.fps <= 0 or args.max_frames == 0:
parser.error("--fps must be positive and --max-frames must be non-zero")
try:
convert(args.input, args.output, args.fps, args.max_frames, args.ffmpeg)
except (OSError, RuntimeError) as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
三、编译烧录以及.bin视频文件传输
1.编译烧录
先将keil文件正常编译、烧录即可。
2.视频文件传输
再打开 PowerShell ,逐行运行脚本如下:
python "你自己放 send_video_uart.py 的路径" `
"你放 video.bin 的路径" `
--port COM3 ` (以实际串口端口为准)
--baud 115200(你实际采用的串口波特率)
若没有报错,应该过一段时间,看到结果如下:
等待 STM32 READY...
擦除并写入 8294400 字节...
8294400/8294400 (100%)
传输完成
完成后,按复位即可成功运行,可在LCD上看到视频。(不知道怎么上传视频,改图片上传了)

3.调试
(1)可以改变任务 StartVideoTask 中 frame_period数值大小,改变额外的帧显示延迟时间,当为0时,机不增加任何的额外延迟时间。
(2)若想修改 st7789 屏幕本身的显示帧频率,应该修改 SPI2 的分频系数 Prescale = 2,将默认的9MHz提升到18MHz,加快读取w25q64的读取速度。
4.总结流程
除了MP4文件,还可尝试将 GIF 或者动态 WebP 转化为 .bin 文件
-> ffmpeg 解码、缩放到 240x240、按指定 FPS 抽帧
-> 转为连续 RGB565 帧:video.bin
-> Python + USB-TTL 串口发送
-> STM32F103C8T6 接收 256 B 页数据
-> SPI2 写入 W25Q64
-> SPI2 从 W25Q64 逐行读取
-> SPI1 发送至 ST7789 LCD 显示
本次实验,主要是熟悉 Flash 数据写入需要注意的“写入地址”、“扇区擦除”问题,以及8PinLCD屏幕的使用,进一步熟悉FreeRTOS。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)