如何制作STM32小电视(基于H7芯片JPEG硬解码和USBFS虚拟串口传输数据)

工程源码下载

为啥小电视需要jpeg解码器

之前做了一个usb虚拟串口python传输原始图像数据的实验发现全速USB的1Mbyte/s传输速率太慢,320x240的图像帧率只有5帧左右,但是没钱换高速USB,只好在解压上面下功夫。实验后发现解码一个5倍压缩的图像只需要10ms,一秒最多能解码100张图片,因此有信心提高小电视的帧率。

结构图

这是小电视的整个数据流图包括以下部分:

  1. JPEG解码器,负责输入文件数据,将文件数据流转换成YCbCr像素数据流
  2. DMA2D数据搬运器,负责将YCbCr数据流转换成RGB数据,并存放在图像缓存区
  3. DMA1负责将RGB图像缓存搬运到spi接口显示屏,显示出来
  4. DMA2负责将USB CDC接收到的数据搬运到jpeg文件缓存区
    5.USB CDC虚拟串口负责接收电脑发来的jpeg文件流
    解码器数据流图

CubeMX配置流程

首先要配置时钟,选择外部晶振HSE 25Mhz,然后设置主频为480Mhz提高性能,最后spi1选择56Mhz(二分频后spi接口频率为28Mhz保证屏幕正常工作)

主时钟配置

主时钟配置
USB时钟配置需要48Mhz刚好h7有个独立的48Mhz时钟源,直接用就行了

USB时钟配置USB时钟配置

核心配置

CPU就配置这第三个地方,注意Dcache要关闭否则可能有数据没到位的bug
CPU配置

spi配置

配置spi1接口用于给屏幕发数据,注意是半双工主机,因为只需要给屏幕发数据,硬件控制NSS(CS)信号,频率在30Mbit左右
在这里插入图片描述

DMA配置

然后是配置DMA1用于给spi发送RGB图像缓存,注意数据宽度最好是byte
DMA1配置
配置DMA2用于将usb接收数据缓存到文件缓存区,用word宽度提高性能
DMA2配置

JPEG配置

最重要的就是配置jpeg解码器,没它啥也干不了
在这里插入图片描述
还要给jpeg设置mdma用于控制输入输出数据流,只需要用add加一个输入一个输出就行了,千万别忘记这步配置mdma,否则HAL_JPEG_Decode_DMA函数因为hjpeg->InDataLength < inXfrSize报错,
在这里插入图片描述
最关键的是配置jpeg中断,否则可能会引发hardfault硬件错误中断函数,按下图勾选就行了
在这里插入图片描述

DMA2D配置

配置dma2d用来接住jpeg输出的ycbcbr数据,模式为memtomem,输出格式rgb565,输入为ycbcr。
在这里插入图片描述

USB配置

当然还要配置usb用来接收python发来的文件,只需要改mode为device_only就行了
在这里插入图片描述
但是还没配置usb设备类为cdc,这属于软件,由下面的配置定义。
在这里插入图片描述

代码部分

做完了配置就该写代码了,按照下面去写

屏幕驱动代码

解码代码运行后 要使用这个程序显示。为什么要分三次启动DMA呢,因为像素太多有76800个,每个两字节,而DMA一次仅能发送65535个。每次发送还要检测spi发完没有否则可能漏发。

void TFT_Clear() {
	// Set full screen window
//    // 1. 设置列地址 0~239
	TFT_SET_ADD(0x0000, 0x0000, TFT_COLUMN_NUMBER - 1, TFT_LINE_NUMBER - 1);//设置全屏窗口
	// 2. 发送全屏像素(连续写入,自动地址递增)
	SCB_CleanDCache();//千万不能在没打开dcache的时候开启 否则硬件错误
	TFT_DC_SET();
	HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*) imgmap, 65534);
	while (HAL_SPI_GetState(&hspi1) != HAL_SPI_STATE_READY);
	HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*) (imgmap + 32767), 65534);
	while (HAL_SPI_GetState(&hspi1) != HAL_SPI_STATE_READY);
	HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*) (imgmap + 65534), 22532);
	while (HAL_SPI_GetState(&hspi1) != HAL_SPI_STATE_READY)	;
}

解码启动函数

调用这个函数就能开启一次jpeg转换 参数为jpeg文件数据 和长度值

uint8_t Jpeg_Decode_Done;
int JPEG_Decode(const uint8_t *jpg_data, uint32_t jpg_len) {
	static uint32_t timecnt;//超时记录
	// 重置标志 
	Jpeg_Decode_Done = 0;
	SCB_CleanDCache();  //关键点从usb来的数据必须清缓存 避免jpeg解码失败
	HAL_JPEG_Abort(&hjpeg);//关闭之前的jpeg解码 防止卡住
	if (HAL_JPEG_Decode_DMA(&hjpeg, (uint8_t*) jpg_data,jpg_len, 
    Jpeg_Output_Buf,
    JPEG_OUTPUT_BUF_SIZE) != HAL_OK) {
		return -1;
	}
	// 等待解码完成
	timecnt = 0;
	while (Jpeg_Decode_Done == 0 && timecnt < 1000) {
		HAL_Delay(1);
		timecnt++;
	};  //超时退出 防止卡在错误中
	return 0;
}

解码输入回调函数

这个函数是处理jpeg输入数据的,NbDecodedData是上一批输入的数据长度,把他们加起来就得出总长度,用于决定这批要输入多少数据,注意jpeg一次最多输入65536字节,如果多于65536个则选择65536,65536由CHUNK_SIZE_IN定义。

void HAL_JPEG_GetDataCallback(JPEG_HandleTypeDef *hjpeg, uint32_t NbDecodedData) {
	uint32_t inDataLength;
	/* 更新已经解码的数据大小 */
	Input_frameIndex += NbDecodedData;

	/* 如果当前已经解码的数据小于总文件大小,继续解码 */
	if (Input_frameIndex < Input_frameSize) {
		/* 更新解码数据位置 */
		JPEGSourceAddress = JPEGSourceAddress + NbDecodedData;

		/* 更新下一轮要解码的数据大小 */
		if ((Input_frameSize - Input_frameIndex) >= CHUNK_SIZE_IN) {
			inDataLength = CHUNK_SIZE_IN;
		} else {
			inDataLength = Input_frameSize - Input_frameIndex;
		}
	} else {
		inDataLength = 0;
	}

	/* 更新输入缓冲 */
	//SCB_CleanDCache();
	//if(inDataLength!=0)
	HAL_JPEG_ConfigInputBuffer(hjpeg, (uint8_t*) JPEGSourceAddress,
			inDataLength);
}

解码输出回调函数

这个函数是处理jpeg输出数据的。参数pDataOut代表数据缓冲,OutDataLength是长度,一般是JPEG_OUTPUT_BUF_SIZE这么长。 pDataOut就是Jpeg_Output_Buf就是jpeg输出的ycbcr数据包(包的大小由前一HAL_JPEG_ConfigOutputBuffer设置的JPEG_OUTPUT_BUF_SIZE值决定,我这里设置为320x8x3列数x块行数x每个像素包含的422采样3个字节)。第一行是输入给dma2d,imgindex是输出的rbg566图像地址,jwidth就是宽度,OutDataLength / (jheight*3)是高度(除以三因为每像素三字节)。imgindex += OutDataLength *2/ 3;这里还要乘二是因为RGB565每像素两字节。第三行HAL_JPEG_ConfigOutputBuffer则设置了下一个包输出的缓冲区和大小

void HAL_JPEG_DataReadyCallback(JPEG_HandleTypeDef *hjpeg, uint8_t *pDataOut,uint32_t OutDataLength) {
			DMA2D_Fill((void*) Jpeg_Output_Buf, (void*) imgindex, jwidth,OutDataLength / (jheight*3), 0);	//DMA2D先遍历所有的width再来height
			imgindex += OutDataLength *2/ 3;	//422一个像素3字节
		    HAL_JPEG_ConfigOutputBuffer(hjpeg, (uint8_t*) Jpeg_Output_Buf,JPEG_OUTPUT_BUF_SIZE);	    //320*204*3 最好是64*3的倍数防止乱码

}

这三个函数如何调用呢,用这句就可以了

JPEG_Decode(jpeg320x240pic0, jpeg320x240pic0len);//jpeg320x240pic0是文件指针 jpeg320x240pic0len是文件字节数

USB输入文件函数

这个就是修改了usb cdc接收函数制作的文件输入函数,Buf是接收帧指针,Len则是字节长度。cdc类一般最多输入64字节(多余字节调用USBD_CDC_ReceivePacket(&hUsbDeviceFS)启动下一轮输入接收),收到64字节后用dma2搬运到文件存储区,这里用了一个双缓冲(两个文件存储区,采用selpic作标志位,selpic==0代表选择jpeg320x240pic10更新数据),接收后文件指针加上长度64。文件末尾最后的包长度一般不是64byte,这时就是。当识别到0x12345678这四个字节代表间隔帧被发送了,这个帧一般会在上一个文件最后一个包发送,因此上一个文件的长度被保存在了jpegcnt中,写入它作为长度。

static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
  /* USER CODE BEGIN 6 */
	int rx_len=*Len;
	int i;
		if(Buf[0]==0x12&&Buf[1]==0x34&&Buf[2]==0x56&&Buf[3]==0x78)
		{//间隔帧

			if(selpic==0)
			{
				jpeg320x240pic0len=jpegcnt;//先处理上个文件
				selpic=1;


				jpeg320x240pic1len=0;
				jpegaddr=(uint32_t)jpeg320x240pic1;
				RXBUF=UserRxBufferFS;
				jpegcnt=0;

			}else
			{
				jpeg320x240pic1len=jpegcnt;//先处理上个文件
				selpic=0;

				jpeg320x240pic0len=0;
				jpegaddr=(uint32_t)jpeg320x240pic0;
				RXBUF=UserRxBufferFS;
				jpegcnt=0;
			}
		}
		else{
		if((jpegcnt+rx_len)<=MAXJPEG){
			HAL_DMA_Start(&hdma_memtomem_dma2_stream0, (uint32_t)RXBUF, (uint32_t)jpegaddr, rx_len);
			HAL_DMA_PollForTransfer(&hdma_memtomem_dma2_stream0, HAL_DMA_FULL_TRANSFER, 100);
		}
			jpegaddr+=rx_len;
			jpegcnt+=rx_len;
		}

//USBD_CDC_SetRxBuffer(&hUsbDeviceFS, RXBUF);
 USBD_CDC_ReceivePacket(&hUsbDeviceFS);

  return (USBD_OK);
  /* USER CODE END 6 */
}

结言

jpeg一项相当有用的技术,使用它能解决单片机多媒体应用中的很多问题,包括传输和存储,而且速度很快,因此学会它是必要的。我在这篇文章中仔细探究jpeg解码的各种问题,有助于帮助各位同僚解决jpeg使用上遇到的困难,也算是做了点有用的事了。
解码效果展示视频
还在更新 敬请期待
参考文献:
[1]stm32h7 jpeg硬解码

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐