esp32 lvgl播放视频于oled,读取SD卡,定时器中断使用
思路:我就用的lvgl传lv_img_dsc_t结构体数据的方式,将图片转换为c矩阵数据保存为.bin文件与SD卡中,然后esp32读取sd卡图片数据,保存于定义的
lv_img_dsc_t变量中,然后将定义的lv_img_dsc_t结构体变量传给lvgl的lv_img控件,以显示图片,定时刷新每一帧图片就完成视频播放的效果。
备注:
1,不知道为什么,再定时器中断函数中读取sd卡,esp32一直重启,原因未知,所以读取sd代码要放在loop()循环里。
2,不知道为什么,将lv_task_handler()放入定时器中断函数中,定时调用,esp32也一直重启,原因未知,所以lv_task_handler();也要放在loop()循环中。
步骤:
1,将视频变为一帧一帧的图片,这个百度很多方法,我就直接网上下载的图片。
2,将图片的分辨率改为自己显示屏的分辨率,我就python自动处理的,因为图片数量有点多,python还可以看一看图片一帧一帧的放出来的视频效果,源码如下:
import time import cv2 if __name__ == '__main__': #测试图片视频效果 # for i in range(5355): # str = 'E:/DC BREAKER/python/Project/ESP32/bad_apple_320_240_25fps/'+'%04d'%(i)+'.jpg' # print("当前显示图片="+str) # img = cv2.imread(str) # cv2.imshow('image', img) # cv2.waitKey(20) #图片分辨率修改 for i in range(5355): str = 'E:/DC BREAKER/python/Project/ESP32/bad_apple_320_240_25fps/'+'%04d'%(i)+'.jpg' print("当前处理图片="+str) img = cv2.imread(str) img_200x200 = cv2.resize(img, (128, 64)) strsave = 'E:/DC BREAKER/python/Project/ESP32/bad_apple_128_64_25fps/' + '%04d' % (i) + '.jpg' cv2.imwrite(strsave, img_200x200) # # 显示一张照片 # img = cv2.imread('E:/DC BREAKER/python/Project/ESP32/bad_apple_320_240_25fps/0000.jpg') # cv2.imshow('image', img) # cv2.waitKey(0)
3,将改好的图片放入官网的图片转c数组在线小工具里,将图片转换为.c文件保存下来,网站如下:
https://lvgl.io/tools/imageconverter
这个工具里面,可以将图片全部选中一起导入进去,它就会自己一个一个图片的将图片数据转换为c数组保存在本地.c文件。
4,将图片的.c文件里的图片数据读出来,然后保存到.bin文件里,这些.bin文件就是保存每一帧图片转换的数据的二进制文件,将他们导入SD卡,然后esp32读取SD卡中的这些.bin文件的图片数据,就可以在lvgl显示图片啦。这里我用的c语言IDE将.c文件自动转化为.bin文件,源码如下:
#include"stdio.h" #include"string.h" //16进制字符串转10进制数 unsigned char strtohex(char str[4]){ char mystrhex[2]; unsigned char a,b; mystrhex[0]=str[2];//10位 mystrhex[1]=str[3];//个位 if(mystrhex[0]>='0'&&mystrhex[0]<='9'){ a=mystrhex[0]-48; } else if(mystrhex[0]>='a'&&mystrhex[0]<='f'){ a=mystrhex[0]-87; } if(mystrhex[1]>='0'&&mystrhex[1]<='9'){ b=mystrhex[1]-48; } else if(mystrhex[1]>='a'&&mystrhex[1]<='f'){ b=mystrhex[1]-87; } return a*16+b; } int main(){ //********************* int t; int i=0; int j=0; int cnt=0; FILE *fp; char mys[4];//用于保存16进制数据字符串 /**************参数修改区**********************************/ unsigned char data[1032];//读取图片数据保存buffer,我的有1032个数据 char filebuffdef[]="E:/DC BREAKER/DEV-CPP/test1/data/"; //读取文件目录 char savefilebuffdef[]="E:/DC BREAKER/DEV-CPP/test1/databin/p"; //保存文件目录 /******************************************************/ char buff[4]; char filebuff[sizeof(filebuffdef)]; char savefilebuff[sizeof(filebuffdef)]; for(j=0;j<374;j++){ printf("开始第%d个文件",j); sprintf(filebuff,filebuffdef); sprintf(buff,"%04d",j); strcat(filebuff,buff); strcat(filebuff,".c"); printf("%s\n",filebuff); fp = fopen(filebuff, "r"); fseek(fp,0L,2); t=ftell(fp); printf("%d\n",t); rewind(fp); char readstr[t]; i=0; while(!feof(fp)){ readstr[i]=fgetc(fp); i++; } fclose(fp); cnt=0; for(i=0;i<sizeof(readstr);i++){ if(readstr[i]=='0'&&readstr[i+1]=='x'){ mys[0]=readstr[i]; mys[1]=readstr[i+1]; mys[2]=readstr[i+2]; mys[3]=readstr[i+3]; data[cnt]=strtohex(mys); cnt++; i=i+3; } } /* 打开文件用于读写 */ sprintf(savefilebuff,savefilebuffdef); sprintf(buff,"%d",j); strcat(savefilebuff,buff); strcat(savefilebuff,".bin"); printf("%s\n",savefilebuff); fp = fopen(savefilebuff, "wb"); /* 写入数据到文件 fread(buffer,size,count,fp);buffer:存放读取到的数据块的数据缓冲区起始地址,size:为函数一次读取的一个数据块的字节长度, count:是所要读取的数据块个数,fp:表示文件指针 */ fwrite(data, sizeof(data), 1, fp); fclose(fp); } //***************** return(0); }
5,esp32读取sd卡图片数据,然后一帧一帧的传入lvgl的lv_img控件,即可实现视频显示,esp32源码如下:
主函数:
#include <lvgl.h> #include "SSD1306Wire.h" // alias for `#include "SSD1306Wire.h"` #include "caiya_gui.h" #include "SD.h" unsigned char time0flag=0;//定时器使用标记寄存器 unsigned char time1flag=0; unsigned char time2flag=0; hw_timer_t *timer = NULL;//定义hw_timer_t 结构类型的指针 hw_timer_t *timer1 = NULL; hw_timer_t *timer2 = NULL; SSD1306Wire display(0x3c, 4, 15); static lv_disp_buf_t disp_buf; static lv_color_t buf[LV_HOR_RES_MAX * 10]; unsigned char buffer[1032]; lv_img_dsc_t myimage= {{LV_IMG_CF_INDEXED_1BIT,0,0,128, 64},1033,buffer}; String filename= "/p";//读取sd卡的文件名存储寄存器 USER_DATA user_data = {{"xixi"},0};//初始化一下 /* Display flushing */ void my_disp_flush(lv_disp_drv_t* disp, const lv_area_t* area, lv_color_t* color_p) { uint32_t w = (area->x2 - area->x1 + 1); uint32_t h = (area->y2 - area->y1 + 1); for (uint16_t i = 0; i < h; i++){ for (uint16_t j = 0; j < w; j++){ if(color_p->full != 0){ display.setPixel(j+area->x1, i+area->y1); } else{ display.clearPixel(j+area->x1, i+area->y1); } color_p++; } } display.display(); lv_disp_flush_ready(disp); } /*读取sd卡文件,lvgl显示函数*/ void mysd() { static int pcnt=0; if(!SD.begin()){ Serial.println("Card Mount Failed"); return; } // Serial.println("SD card Ready!"); // Serial.printf("SD.cardSize = %lld \r\n", SD.cardSize()); // Serial.printf("SD.cardType = %d \r\n", SD.cardType()); filename= "/badapple/p"; filename.concat(pcnt); filename.concat(".bin"); File file = SD.open(filename, FILE_READ); // Serial.printf("is there /badapple/p1.bin? :%d \r\n", SD.exists("/badapple/p1.bin")); file.read(buffer,1032); file.close(); SD.end(); myimage.data=buffer; lv_img_set_src(img1, &myimage); if(pcnt==374)pcnt=0; else pcnt++; } // 函数名称:onTimer() // 函数功能:中断服务的功能,它必须是一个返回void(空)且没有输入参数的函数 // 为使编译器将代码分配到IRAM内,中断处理程序应该具有 IRAM_ATTR 属性 void IRAM_ATTR TimerEvent() { if(time0flag==0)time0flag=1; else time0flag=0; } //定时器1中断服务函数 void IRAM_ATTR Timer1Event(){ if(time1flag==0)time1flag=1; else time1flag=0; } //定时器2中断服务函数 void IRAM_ATTR Timer2Event(){ if(time2flag==0){ digitalWrite(2,0); time2flag=1; } else{ digitalWrite(2,1); time2flag=0; } } void setup() { Serial.begin(115200); /* prepare for possible serial debug */ pinMode(2,OUTPUT); digitalWrite(2,1); pinMode(0,INPUT_PULLUP); lv_init(); display.init(); lv_disp_buf_init(&disp_buf, buf, NULL, LV_HOR_RES_MAX * 10); /*Initialize the display*/ lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); disp_drv.hor_res = 128; disp_drv.ver_res = 64; disp_drv.flush_cb = my_disp_flush; disp_drv.buffer = &disp_buf; lv_disp_drv_register(&disp_drv); set_caiya_gui(); lv_scr_load_anim(scr1, LV_SCR_LOAD_ANIM_MOVE_RIGHT, 500, 5000, false); // 加载屏幕TWO,动画效果为LV_SCR_LOAD_ANIM_FADE_ON,切换时间为500ms,延迟5000ms后从第一屏开始切换,切换完成后删除屏幕一 /*函数名称:timerBegin() 函数功能:Timer初始化,分别有三个参数 函数输入:1. 定时器编号(0到3,对应全部4个硬件定时器) 2. 预分频器数值(ESP32计数器基频为80M,80分频单位是微秒) 3. 计数器向上(true)或向下(false)计数的标志 函数返回:一个指向 hw_timer_t 结构类型的指针*/ timer = timerBegin(0, 80, true); /*函数名称:timerAttachInterrupt() 函数功能:绑定定时器的中断处理函数,分别有三个参数 函数输入:1. 指向已初始化定时器的指针(本例子:timer) 2. 中断服务函数的函数指针 3. 表示中断触发类型是边沿(true)还是电平(false)的标志 函数返回:无*/ timerAttachInterrupt(timer, &TimerEvent, true); /*函数名称:timerAlarmWrite() 函数功能:指定触发定时器中断的计数器值,分别有三个参数 函数输入:1. 指向已初始化定时器的指针(本例子:timer) 2. 第二个参数是触发中断的计数器值(1000000 us -> 1s) 3. 定时器在产生中断时是否重新加载的标志 函数返回:无*/ timerAlarmWrite(timer, 20000, true); timerAlarmEnable(timer); // 使能定时器 timer1 = timerBegin(1, 80, true); timerAttachInterrupt(timer1, &Timer1Event, true); timerAlarmWrite(timer1, 5000, true); timerAlarmEnable(timer1); // 使能定时器 timer2 = timerBegin(2, 80, true); timerAttachInterrupt(timer2, &Timer2Event, true); timerAlarmWrite(timer2, 500000, true); timerAlarmEnable(timer2); // 使能定时器 } int flag = 0; unsigned char playflag=0;//播放视频标记位 void loop() { if(time1flag==1)lv_task_handler(); /* let the GUI do its work */ if((time0flag==1)&&(playflag==1))mysd(); if(digitalRead(0)==0) { while(digitalRead(0)==0); if(flag==1)flag=0; else flag++; Serial.println(flag); if(flag==1){ playflag=0; lv_scr_load_anim(scr2, LV_SCR_LOAD_ANIM_MOVE_RIGHT, 500, 0, false); } else if(flag==0){ playflag=1; lv_scr_load_anim(scr1, LV_SCR_LOAD_ANIM_MOVE_RIGHT, 500, 0, false); } //手动发送事件 //方式 1:发送用户自定义事件,同时携带用户自定义数据 user_data.age=(unsigned char)flag; Serial.println(user_data.age); lv_event_send(label2,USER_EVENT_1,&user_data); } }
其他源文件参考上一篇博客,都差不多的,只是将定义的lv_obj_t* img1;变量声明了一下,让其可以在主程序中被调用。
***********************************************************************************************
SD的库中默认SD卡用vspi与esp32通信,这个在SD.h中源码一看便知:
SS表示IO5,在Pins_Arduino.h中可以查到:
SPI在SPI.cpp中有赋值:
可以看到默认给的是VSPI参数,然后在SPI.cpp中的这个函数即可看到默认值是怎么赋值的,修改管脚也是在这儿弄。
所以如果要是用HSPI,需要如下代码即可:
HSPI的默认MISO引脚是12,而12在ESP32中是用于上电时设置flash电平的,上电之前上拉会导致芯片无法启动,因此我们需要将默认的引脚换一个,比如替换为26。