摘要:本项目基于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.hw25q64.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。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐