【STM32H7教程】STM32如何解码JPEG以及实现USB图传
如何制作STM32小电视(基于H7芯片JPEG硬解码和USBFS虚拟串口传输数据)
为啥小电视需要jpeg解码器
之前做了一个usb虚拟串口python传输原始图像数据的实验发现全速USB的1Mbyte/s传输速率太慢,320x240的图像帧率只有5帧左右,但是没钱换高速USB,只好在解压上面下功夫。实验后发现解码一个5倍压缩的图像只需要10ms,一秒最多能解码100张图片,因此有信心提高小电视的帧率。
结构图
这是小电视的整个数据流图包括以下部分:
- JPEG解码器,负责输入文件数据,将文件数据流转换成YCbCr像素数据流
- DMA2D数据搬运器,负责将YCbCr数据流转换成RGB数据,并存放在图像缓存区
- DMA1负责将RGB图像缓存搬运到spi接口显示屏,显示出来
- DMA2负责将USB CDC接收到的数据搬运到jpeg文件缓存区
5.USB CDC虚拟串口负责接收电脑发来的jpeg文件流
CubeMX配置流程
首先要配置时钟,选择外部晶振HSE 25Mhz,然后设置主频为480Mhz提高性能,最后spi1选择56Mhz(二分频后spi接口频率为28Mhz保证屏幕正常工作)
主时钟配置

USB时钟配置需要48Mhz刚好h7有个独立的48Mhz时钟源,直接用就行了
USB时钟配置
核心配置
CPU就配置这第三个地方,注意Dcache要关闭否则可能有数据没到位的bug
spi配置
配置spi1接口用于给屏幕发数据,注意是半双工主机,因为只需要给屏幕发数据,硬件控制NSS(CS)信号,频率在30Mbit左右
DMA配置
然后是配置DMA1用于给spi发送RGB图像缓存,注意数据宽度最好是byte
配置DMA2用于将usb接收数据缓存到文件缓存区,用word宽度提高性能
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硬解码
更多推荐


所有评论(0)