l#介绍#

使用STM32F103VET6开发板。

屏幕为tft-lcd2.4寸:240×320。

python脚本自动切割视频为帧bin文件

python脚本连续下发bin文件


简单分析:

两个字节代表一个像素点(因为是rgb3色,不懂的同学补习下)。这里我设置的是宽192高80,所以总的字节是192*80*2=30720个字节,下面153600不用管这是另一张图片的数据。然后通过轮询显示图片就可以达到视频效果了

两年半

实现过程

先通过硬件SPI+串口DMA来显示图片

        使用串口1DMA接收数据,在中断完成回调加入环形缓冲区,buffer1和buff2来回接收,这个30720是一张图片的大小,记得我这里使用的是DMA正常模式所以在初始化的时候要 HAL_UART_Receive_DMA(&huart1, buffer2, FRAME_SIZE); 

 bufferReady这个是完成标志位,可以在其他线程中判断当前标志位来刷新图片

#define FRAME_SIZE 30720  // 每帧大小
uint8_t buffer1[FRAME_SIZE];  // 缓冲区1
uint8_t buffer2[FRAME_SIZE];  // 缓冲区2
uint8_t *rxBuffer = buffer1;  // 当前接收缓冲区
uint8_t *displayBuffer = buffer2;  // 当前显示缓冲区
volatile uint8_t bufferReady = 0;  // 缓冲区切换标志


void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart == &huart1)
    {
        // 切换缓冲区
        if (rxBuffer == buffer1)
        {
		
            rxBuffer = buffer2;
            HAL_UART_Receive_DMA(&huart1, buffer2, FRAME_SIZE);
        }
        else
        {
            rxBuffer = buffer1;
            HAL_UART_Receive_DMA(&huart1, buffer1, FRAME_SIZE);
        }
		
        bufferReady = 1;  // 通知主循环数据已准备好
    }
}

在while(1)中刷新屏幕 

 while (1)
  {

	  if (bufferReady)
        {
		
            // 切换显示缓冲区
            uint8_t *temp = displayBuffer;
            displayBuffer = rxBuffer;
            rxBuffer = temp;
            bufferReady = 0;

            // 刷新显示
       	  	 LCD_Address_Set(0, 0, 192, 80);
		   LCD_ShowPicture(0,0,192,80,displayBuffer);
		   printf("FPS:%d",Get_FPS());
        }
	
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }

     LCD_Address_Set(0, 0, 192, 80);是设置lcd需要去刷新的位置

/******************************************************************************
      函数说明:设置起始和结束地址
      入口数据:x1,x2 设置列的起始和结束地址
                y1,y2 设置行的起始和结束地址
      返回值:  无
******************************************************************************/
void LCD_Address_Set(u16 x1,u16 y1,u16 x2,u16 y2)
{
		LCD_WR_REG(0x2a);//列地址设置
		LCD_WR_DATA(x1);
		LCD_WR_DATA(x2);
		LCD_WR_REG(0x2b);//行地址设置
		LCD_WR_DATA(y1);
		LCD_WR_DATA(y2);
		LCD_WR_REG(0x2c);//储存器写
}

LCD_WR_DATA是一次写入两个字节

/******************************************************************************
      函数说明:LCD写入数据
      入口数据:dat 写入的数据
      返回值:  无
******************************************************************************/
void LCD_WR_DATA(u16 dat)
{
//	 uint8_t dat_h=dat>>8;//高字节
//     HAL_SPI_Transmit(&hspi1,&dat_h, 1, 0xff);
//	uint8_t  dat_l=dat&0xff;//低字节
//	 HAL_SPI_Transmit(&hspi1,&dat_l, 1, 0xff);
	
	LCD_Writ_Bus(dat>>8);
	LCD_Writ_Bus(dat);

}

LCD_Writ_Bus函数,写入数据处理

/******************************************************************************
      函数说明:LCD串行数据写入函数
      入口数据:dat  要写入的串行数据
      返回值:  无
******************************************************************************/
void LCD_Writ_Bus(u8 dat) 
{	
 
	LCD_CS_Clr();
	
	HAL_SPI_Transmit(&hspi1,&dat,1,100);
	if (HAL_SPI_Transmit(&hspi1, &dat, 1, 1000) != HAL_OK) {
        // 处理错误(如重试或报错)
       Error_Handler();
		
    }
	LCD_CS_Set();
}

LCD_ShowPicture函数,设置起点图片高宽,和上位机发送的bin文件数据

/******************************************************************************
      函数说明:显示图片
      入口数据:x,y起点坐标
                length 图片长度
                width  图片宽度
                pic[]  图片数组    
      返回值:  无
******************************************************************************/

void LCD_ShowPicture(u16 x, u16 y, u16 length, u16 width, const u8 pic[]) {
    uint32_t num = length * width * 2; // 总字节数
    uint32_t offset = 0;               // 数据偏移量

    LCD_Address_Set(x, y, x + length - 1, y + width - 1);
    LCD_CS_Clr();

    while (num > 0) {
        uint32_t chunk_size = (num > 65534) ? 65534 : num; // 分块大小
        HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*)(pic + offset), chunk_size); // 启动 DMA 传输

        // 等待 DMA 传输完成
        while (HAL_DMA_GetState(&hdma_spi1_tx) != HAL_DMA_STATE_READY) {
            // 空循环,等待传输完成
        }

        num -= chunk_size;  // 更新剩余字节数
        offset += chunk_size; // 更新数据偏移量
    }

    LCD_CS_Set();
   // HAL_Delay(10); // 动态调整延时
}

main.c完整代码内容

        

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2025 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "dma.h"
#include "rtc.h"
#include "spi.h"
#include "tim.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "lcd.h"
#include "lcd_init.h"
#include "stdio.h"
#include "string.h"
#include "pic.h"
#include <stdlib.h>
int fputc(int ch, FILE *f)
{
	HAL_UART_Transmit(&huart5, (uint8_t *)&ch, 1, 0xffff);
	return ch;
}

/**
 * ????????: ?????c????getchar,scanf??DEBUG_USARTx
 * ???????: ??
 * ?? ?? ?: ??
 * ?    ??????
 */
int fgetc(FILE *f)
{
	uint8_t ch = 0;
	HAL_UART_Receive(&huart5, &ch, 1, 0xffff);
	return ch;
}
#define  RX_MAXLEN 128
#define TX_MAXLEN 128
typedef struct{

 uint8_t RxBuf[RX_MAXLEN]; // ???????
  uint8_t TxBuf[TX_MAXLEN]; // ???????
  uint16_t RxCnt;           // ???????????
  uint32_t RxLen;           // ???????????
  uint16_t TxLen;
  uint8_t RxStart; // ?????????
  uint8_t RxFlag;  // ??????????????



}Uart_Tpye_t; 
Uart_Tpye_t Uart3,Uart5;

uint8_t Uart1RevByte,Uart3RevByte,Uart5RevByte;
extern DMA_HandleTypeDef hdma_usart1_rx;
void UART_IDLECallBack(UART_HandleTypeDef *huart)
{

}
#define FRAME_SIZE 30720  // 每帧大小
uint8_t buffer1[FRAME_SIZE];  // 缓冲区1
uint8_t buffer2[FRAME_SIZE];  // 缓冲区2
uint8_t *rxBuffer = buffer1;  // 当前接收缓冲区
uint8_t *displayBuffer = buffer2;  // 当前显示缓冲区
volatile uint8_t bufferReady = 0;  // 缓冲区切换标志


void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart == &huart1)
    {
        // 切换缓冲区
        if (rxBuffer == buffer1)
        {
		
            rxBuffer = buffer2;
            HAL_UART_Receive_DMA(&huart1, buffer2, FRAME_SIZE);
        }
        else
        {
            rxBuffer = buffer1;
            HAL_UART_Receive_DMA(&huart1, buffer1, FRAME_SIZE);
        }
		
        bufferReady = 1;  // 通知主循环数据已准备好
    }
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    /* USER CODE BEGIN Callback 0 */

    /* USER CODE END Callback 0 */
	  if (htim->Instance == TIM2)
    {
        FPS = FPS_Count;
        FPS_Count = 0;
    }
    /* USER CODE BEGIN Callback 1 */

    /* USER CODE END Callback 1 */
}
 uint8_t Cursor_posX=8,Cursor_posY=16;

void U5task()
{
}


/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
extern uint8_t OLED_DisplayBuf[12][192];//320*240
RTC_DateTypeDef GetData;  //获取日期结构体
RTC_TimeTypeDef GetTime;   //获取时间结构体
/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{

  /* USER CODE BEGIN 1 */
  uint16_t  wid_index=0,hei_index=0;
  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_TIM2_Init();
  MX_UART5_Init();
  MX_USART1_UART_Init();
  MX_SPI1_Init();
  MX_RTC_Init();
  /* USER CODE BEGIN 2 */
   LCD_Init();
   HAL_TIM_Base_Start_IT(&htim2);
   //__HAL_UART_ENABLE_IT(&huart5, UART_IT_IDLE);

  Uart5.RxLen = 0;

  HAL_UART_Receive_DMA(&huart1, buffer1, FRAME_SIZE);
  HAL_UART_Receive_IT(&huart5,&Uart5RevByte, 1);
  LCD_Fill(0,0,LCD_W,LCD_H,BLACK); 
 
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {

	  if (bufferReady)
        {
		
            // 切换显示缓冲区
            uint8_t *temp = displayBuffer;
            displayBuffer = rxBuffer;
            rxBuffer = temp;
            bufferReady = 0;

            // 刷新显示
       	  	 LCD_Address_Set(0, 0, 192, 80);
		   LCD_ShowPicture(0,0,192,80,displayBuffer);
		   printf("FPS:%d",Get_FPS());
        }
	
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
  RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSI|RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.LSIState = RCC_LSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
  PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC;
  PeriphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSI;
  if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
  {
    Error_Handler();
  }
}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

使用python去截取视频的每一帧,输出到文件夹

下面是代码,建议使用python3.9.8(依赖可以一次成功)

import cv2
import numpy as np
import os
import struct
import PySimpleGUI as sg
from threading import Thread

# 配置参数
TARGET_WIDTH = 192  # 目标宽度
TARGET_HEIGHT = 80  # 目标高度
FRAME_PREFIX = "frame"  # 帧文件前缀


def rgb24_to_rgb565(bgr_frame):
    """将BGR24图像转换为RGB565高位在前格式"""
    # 转为RGB格式
    rgb_frame = cv2.cvtColor(bgr_frame, cv2.COLOR_BGR2RGB)

    # 提取RGB通道(优化向量化运算)
    r = (rgb_frame[..., 0] >> 3).astype(np.uint16)  # 5-bit
    g = (rgb_frame[..., 1] >> 2).astype(np.uint16)  # 6-bit
    b = (rgb_frame[..., 2] >> 3).astype(np.uint16)  # 5-bit

    # 合并为RGB565(高位在前)
    rgb565 = (r << 11) | (g << 5) | b
    return rgb565


def center_crop_and_resize(frame, target_width, target_height):
    """居中裁剪并调整到目标尺寸"""
    h, w = frame.shape[:2]
    aspect_ratio = target_width / target_height

    # 计算裁剪区域
    if w / h > aspect_ratio:
        # 裁剪宽度
        new_width = int(h * aspect_ratio)
        start_x = (w - new_width) // 2
        cropped = frame[:, start_x:start_x + new_width]
    else:
        # 裁剪高度
        new_height = int(w / aspect_ratio)
        start_y = (h - new_height) // 2
        cropped = frame[start_y:start_y + new_height, :]

    # 调整到目标尺寸
    resized = cv2.resize(cropped, (target_width, target_height), interpolation=cv2.INTER_LINEAR)
    return resized


def process_video(input_path, output_dir, progress_callback):
    """视频处理主逻辑"""
    cap = cv2.VideoCapture(input_path)
    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    frame_count = 0

    # 创建输出目录
    os.makedirs(output_dir, exist_ok=True)

    while cap.isOpened():
        ret, frame = cap.read()
        if not ret:
            break

        # 居中裁剪并调整到目标尺寸
        resized = center_crop_and_resize(frame, TARGET_WIDTH, TARGET_HEIGHT)

        # 转为RGB565
        rgb565_data = rgb24_to_rgb565(resized)

        # 转换为高位在前的二进制数据
        binary_data = bytearray()
        for pixel in rgb565_data.flatten():
            # 使用大端字节序(高位在前)
            binary_data += struct.pack('>H', pixel)

        # 写入文件
        output_path = os.path.join(output_dir, f"{FRAME_PREFIX}_{frame_count:04d}.bin")
        with open(output_path, 'wb') as f:
            f.write(binary_data)

        frame_count += 1
        progress_callback(frame_count, total_frames)

    cap.release()
    return frame_count


class VideoProcessor(Thread):
    """视频处理线程(防止GUI卡顿)"""

    def __init__(self, input_path, output_dir, progress_callback):
        super().__init__()
        self.input_path = input_path
        self.output_dir = output_dir
        self.progress_callback = progress_callback

    def run(self):
        try:
            total = process_video(self.input_path, self.output_dir, self.progress_callback)
            sg.popup_ok(f"转换完成!共生成 {total} 帧", title="完成")
        except Exception as e:
            sg.popup_error(f"处理失败: {str(e)}")


def main():
    sg.theme('DarkTeal2')

    layout = [
        [sg.Text("输入视频:"), sg.Input(key='-INPUT-'), sg.FileBrowse(file_types=(("视频文件", "*.mp4 *.avi *.mov"),))],
        [sg.Text("输出目录:"), sg.Input(key='-OUTPUT-'), sg.FolderBrowse()],
        [sg.ProgressBar(100, size=(50, 20), key='-PROGRESS-')],
        [sg.Button("开始转换", key='-START-'), sg.Button("退出")]
    ]

    window = sg.Window('视频转二进制帧工具', layout)
    processor = None

    while True:
        event, values = window.read(timeout=100)
        if event in (sg.WIN_CLOSED, '退出'):
            if processor and processor.is_alive():
                processor.join()
            break

        if event == '-START-':
            if not values['-INPUT-'] or not values['-OUTPUT-']:
                sg.popup_error("请选择输入视频和输出目录!")
                continue

            # 禁用按钮防重复点击
            window['-START-'].update(disabled=True)

            # 启动处理线程
            def update_progress(current, total):
                percent = int(current / total * 100)
                window['-PROGRESS-'].update(percent)

            processor = VideoProcessor(
                values['-INPUT-'],
                values['-OUTPUT-'],
                update_progress
            )
            processor.start()

    window.close()


if __name__ == '__main__':
    main()

裁剪完视频后就会生成很多个bin文件,通过python来写一个串口发送给下位机

      

实现代码如下同样使用python3.9.8版本最好

import serial
import serial.tools.list_ports
import os
import time
import PySimpleGUI as sg
from threading import Thread

sg.theme('DarkBlue')

# 配置参数
CHUNK_SIZE = 65535   # 增大块大小(减少传输次数)
BAUDRATE = 2000000   # 提升波特率(需硬件支持)
FRAME_DELAY = 0.033  # 目标帧率 30 FPS → 0.033秒/帧


def list_serial_ports():
    """获取可用串口列表"""
    ports = serial.tools.list_ports.comports()
    return [port.device for port in ports]


class FrameSender(Thread):
    def __init__(self, port, baudrate, file_paths, window):
        super().__init__()
        self.port = port
        self.baudrate = baudrate
        self.file_paths = file_paths
        self.window = window
        self.running = False  # 播放状态标志位

    def run(self):
        try:
            ser = serial.Serial(self.port, self.baudrate, timeout=0)
            self.window['-LOG-'].print(f"⚡ 串口已连接: {self.port} @ {self.baudrate} bps")

            self.running = True  # 开始播放
            while self.running:
                for current_frame in range(len(self.file_paths)):
                    if not self.running:
                        break  # 如果停止播放,退出循环

                    start_time = time.time()
                    file_path = self.file_paths[current_frame]

                    # 全速发送单帧
                    with open(file_path, 'rb') as f:
                        data = f.read()
                        ser.write(data)  # 一次性写入(非分块)
                        ser.flush()      # 确保数据完全发送

                    # 更新状态
                    self.window['-LOG-'].print(f"▶ 已发送帧 {current_frame + 1}/{len(self.file_paths)}")
                    self.window['-PROGRESS-'].update((current_frame + 1) / len(self.file_paths) * 100)

                    # 动态计算延迟(维持目标帧率)
                    elapsed = time.time() - start_time
                    delay = max(FRAME_DELAY - elapsed, 0)
                    time.sleep(delay)

            ser.close()
            self.window['-LOG-'].print("✅ 播放已停止!")
        except Exception as e:
            sg.popup_error(f"错误: {str(e)}")

    def stop(self):
        self.running = False  # 停止播放


def main():
    layout = [
        [sg.Text("串口:"), sg.Combo(list_serial_ports(), key='-PORT-'),
         sg.Text("波特率:"), sg.Input(BAUDRATE, key='-BAUD-', size=10)],
        [sg.Text("帧文件:"), sg.Input(key='-FILES-'), sg.FilesBrowse(file_types=(("BIN", "*.bin"),))],
        [sg.ProgressBar(100, size=(50, 20), key='-PROGRESS-')],
        [sg.Multiline(size=(70, 15), key='-LOG-', autoscroll=True, disabled=True)],
        [sg.Button("开始播放", key='-PLAY-'), sg.Button("停止", key='-STOP-'), sg.Button("退出")]
    ]

    window = sg.Window('高速帧播放工具', layout)
    sender = None

    while True:
        event, values = window.read(timeout=100)
        if event in (sg.WIN_CLOSED, '退出'):
            if sender:
                sender.stop()  # 停止播放线程
                sender.join()  # 等待线程结束
            break

        if event == '-PLAY-':
            if sender and sender.is_alive():
                sg.popup("警告: 已有发送任务运行中!")
                continue

            file_paths = values['-FILES-'].split(';') if values['-FILES-'] else []
            if not file_paths:
                sg.popup_error("请选择帧文件!")
                continue

            sender = FrameSender(values['-PORT-'], int(values['-BAUD-']), file_paths, window)
            sender.start()

        if event == '-STOP-':
            if sender:
                sender.stop()  # 停止播放线程

    window.close()


if __name__ == '__main__':
    main()

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐