stm32串口UART发送图片tft-lcd显示,通过提高帧率播放视频
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()
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)