尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于OV7670和FIFO的TFT实时预览:嵌入式显示调试实战

基于OV7670和FIFO的TFT实时预览:嵌入式显示调试实战 最近在调一个Camera demo project for testing TFT preview说白了就是让摄像头把画面实时送到TFT彩屏上显示。这个项目听起来不像什么大工程但真做起来从摄像头输出格式、FIFO缓冲、SPI传输到屏幕刷新每一环都能让你磨掉一整天。我这次用的是一块国产低功耗MCU、一颗非常经典的OV7670摄像头模块以及一块SPI接口的小尺寸TFT屏。整个调试过程几乎把嵌入式常见的坑都踩了一遍所以这篇文章不只是记录demo怎么跑起来更想把我选型时怎么想、驱动怎么拆、故障怎么定位的思路完整理一遍。如果你手里也有类似的“摄像头显示”需求不管是从零开始还是想把一个半成品调通这份经验应该都能帮上忙。1. 为什么会有这个Demo从测试工装到TFT预览1.1 项目需求是怎么来的这年头给产品加屏幕已经不是什么新鲜事但屏幕这一块“能不能稳定显示”往往不是芯片厂商给你一个参考例程就能解决的。尤其是TFT彩屏走SPI接口的屏便宜、好用但一旦刷新帧率上不去、颜色不对、图像撕裂排查起来非常痛苦。我手上的项目并不是直接做一个相机产品而是一个硬件测试工装需要在产线上通过屏幕显示摄像头采集的画面用来快速判断摄像头模组是否正常、TFT屏的显示链路是否通。说白了这是一个“供测试使用的TFT预览工程”。它不追求高帧率、不追求漂亮的UI核心目标是一条确定的图像链路——摄像头采集、数据缓存、SPI送屏——能不能稳定跑起来跑起来之后还有多少余量可以给上层应用用。在这个目标下我把它拆成了一个最小的Camera demo project只保留最基础的拍照、取图、显示逻辑但把驱动层全部铺开方便后续调整任何一段硬件。1.2 Demo要解决的核心问题既然是“for testing TFT preview”那首当其冲的是验证TFT屏本身。新贴的屏幕、新画的PCB、新接的背光都可能因为焊接虚焊、信号线过长、SPI时序不匹配导致显示异常。如果用了一个复杂的摄像头实时预览程序一旦屏幕花了你根本分不清是摄像头的问题还是TFT的问题。所以我第一步先让一个纯色块或简单的测试画面在屏幕上跑通确认TFT驱动没问题之后再把摄像头数据接进来。这套流程看上去迂回但确实能大幅缩小排查范围。很多第一次做摄像头预览的人总想一口气把摄像头和屏幕全部打通结果花屏之后只能干瞪眼。我这次在验证完TFT之后才去初始化摄像头后面图像的每一步都有参照物排查效率高很多。1.3 这个Demo适合谁参考如果你是嵌入式开发的新手想学习怎么驱动SPI TFT屏幕或者怎么读一个并行摄像头模块这个项目可以作为一个入门到进阶的完整案例如果你已经做过单屏或单摄像头但一直没把两者串起来也可以参考这里的数据通路设计。尤其是那些不想用昂贵开发板想用低端MCU做低分辨率图像显示的场景这篇文章里的选型和优化思路会很有价值。2. 硬件选型为什么用OV7670FIFO模块和HC32L0722.1 摄像头经典OV7670带FIFO模块摄像头这一环很多人第一反应是OV2640、OV5640这一类高分辨率Sensor但对于一个TFT预览测试项目我推荐OV7670而且是带FIFO缓冲的模块版本。原因有三。第一OV7670数据手册非常公开寄存器说明齐全网上成功案例多哪怕你之前完全没碰过摄像头驱动也能找到大量参考资料。第二OV7670的输出可以在RGB565、RGB444、YUV等格式间切换分辨率可以做到320x240的QVGA刚好和常见的1.8寸、2.4寸TFT屏匹配。第三也是最关键的很多卖家配套的模块上集成了AL422B FIFO芯片。AL422B是一个384K字节的FIFO。有了它摄像头可以连续地把图像像素写入FIFOMCU不需要实时跟踪每一个像素时钟而是可以等一帧写完后再慢慢读出来。这等于把“实时采集”变成了“按帧搬砖”大大降低了对MCU性能和实时性的要求。没有FIFO的裸摄像头虽然也可以做但需要MCU有足够的DMA资源并且严格同步像素时钟调试难度会直线上升。2.2 MCUHC32L072够不够用很多读者看到HC32L072可能会问这不是一颗低功耗控制型MCU吗用来做图像显示是不是太勉强了确实如果做实时显示、人脸识别之类的应用这颗芯片肯定不合适。但我们这里的目标是“测试TFT预览”不是“流畅视频播放”所以只要能实现一帧一帧读取、刷新任务就完成了。HC32L072主频最高可以到64MHz左右不同封装和型号略有差异SRAM从8KB到几十KB可选。如果直接缓存一整帧QVGA RGB565数据3202402153600字节那得选大封装高配置版本。为了让普通型号也能跑我实际操作时没有在MCU里缓存完整帧而是直接从FIFO读数、边读边往TFT发送。这样MCU只需要一块很小的行缓冲甚至可以不缓存整帧从而绕开SRAM不足的问题。同时HC32L072自带硬件SPI、DMA、定时器和多个通用GPIOSPI发送可以用DMA减轻CPU负担。对一颗只做预览的MCU来说这个配置算很均衡了。如果换到STM32F103这种经典MCU其实思路也完全一样只是寄存器配置不同。2.3 TFT屏幕ST7735S和ILI9341怎么选TFT屏幕我用了两块一块1.8寸ST7735S一块2.4寸ILI9341都是SPI接口。ST7735S最大分辨率一般是128x160ILI9341是320x240刚好匹配OV7670输出QVGA。做测试时我建议先上128x160的ST7735S因为它像素少刷新压力小调通后可以放大画面到2.4寸屏。这样能避免一开始就卡在高分辨率刷新率上分不清是驱动问题还是性能问题。有一点要注意ST7735S和ILI9341即使是同一厂家同型号初始化命令序列也可能因为封装批次不同而有细微差异。所以拿到屏幕的第一件事不是写代码而是确认屏的具体型号、驱动IC版本然后去原厂或者靠谱渠道找初始化序列。我把硬件连接整理成一张表方便你在选型时对照外设引脚/信号接MCU引脚说明摄像头模块SIOC / SIODI2C时钟/数据OV7670寄存器配置摄像头模块VSYNCGPIO输入帧同步信号摄像头模块FIFO WRSTGPIO输出FIFO写指针复位摄像头模块FIFO RRSTGPIO输出FIFO读指针复位摄像头模块FIFO OEGPIO输出FIFO输出使能摄像头模块FIFO RCLKGPIO输出FIFO读时钟摄像头模块DOUT[7:0]GPIO输入像素并行数据TFT屏SCKSPI时钟硬件SPI时钟TFT屏MOSISPI主机输出数据线TFT屏CSGPIO输出片选TFT屏DCGPIO输出数据/命令选择TFT屏RSTGPIO输出复位TFT屏BLKGPIO/PWM背光2.4 为什么不用带DVP接口的MCU很多有一定资源的MCU会直接带DVP数字摄像头接口可以自动把摄像头并行数据搬进内存。HC32L072没有DVPSTM32F4系列很多型号也没有DVP。如果你一定想用DVP那得换STM32H7、NXP i.MX RT等等。但是在我们这个测试场景里DVP不是必需品。DVP虽然省心但它会引入另一个问题MCU需要专门的DMA通道去接收像素数据还要处理VSYNC、HREF、PCLK时序而且如果摄像头分辨率太高DMA带宽会变得很紧张。而FIFO方案把摄像头“排队”的活交给了AL422BMCU只要在空闲时慢慢读数据就行这让调试难度下降了一个档次。所以我个人强烈建议除非你的产品必须实时预览且帧率要求高否则测试阶段优先选FIFO模块。3. 打通TFT屏初始化序列与SPI刷屏的五个细节3.1 SPI时钟极性、相位与速率TFT屏的SPI接口一般比较宽容但ST7735S和ILI9341官方手册都要求SPI Mode 0或Mode 3。我这次用的是SPI Mode 0CPOL0、CPHA0也就是时钟空闲为低、数据在上升沿采样。如果选错屏幕大概率白屏或者出现随机噪点。速率方面理论上ST7735S可以跑很高但受限于MCU的硬件SPI模块以及PCB布线实际跑20MHz左右比较稳妥。我用HC32L072的硬件SPI时会把分频系数配置成使SPI时钟尽量接近20MHz。速率太高会出噪声太低会导致刷新一帧太久刷新慢到能肉眼看到屏幕从下往上扫描体验极差。3.2 初始化序列要的是稳定不是花哨TFT屏幕的初始化代码并不难但必须严格按顺序发送指定命令。以ST7735S为例通常先复位、然后SLPOUT、等待足够时间再设置帧率格式、颜色格式、显示方向、Gamma曲线、点亮显示等。我见过很多新手把初始化序列直接从网上复制发现显示颜色不对之后第一反应是改代码里的颜色宏但其实是初始化时序里某个Delay时间不够。ST7735S和ILI9341内部的DCDC和时序校准都需要时间尤其是SLPOUT之后至少要等待120ms否则后续命令不生效。我有一次调试时屏幕一直白屏最后发现就是Reset信号拉长后没有等足够久导致初始化序列没能正确写入。3.3 像素格式和颜色字节序TFT屏和摄像头都有颜色格式的设置。ST7735S支持RGB565和RGB666等格式OV7670也可以输出RGB565。看起来都是RGB565但只要端序不一致显示画面就会偏蓝偏红甚至发绿。最典型的坑是OV7670输出的RGB565是大端序即高字节在前而有些TFT屏默认希望SPI先收到高字节再收到低字节。如果两端都配置成同一格式问题不大如果摄像头配置成了RGB565小端屏幕那边又按大端解析颜色就全乱了。我建议在设计驱动时把像素字节序统一封装成一个宏后面调试颜色时只需要改一处。比如#define PIXEL_FORMAT_RGB565 1 #define RGB565(r, g, b) ((((r) 0xF8) 8) | (((g) 0xFC) 3) | ((b) 3))刷屏时先发高字节再发低字节这样与大多数SPI TFT的预期一致。3.4 窗口地址和扫描方向窗口地址是TFT屏最容易出问题的地方。如果你没有正确设置Column Address和Row Address而是直接往显存里写数据那么画面会出现整体偏移、错位或者呈斜线状分布。比如ILI9341的CASET、RASET命令写完之后还要把X、Y坐标范围转成对应的5位或8位参数不同驱动IC的坐标习惯可能还不一样。还有扫描方向。竖屏、横屏、镜像、旋转分别对应MX、MY、MV这几个bit的组合。在做摄像头显示时最好把屏幕扫描方向设置成和摄像头输出的图像方向一致否则你还要额外做一次坐标旋转不仅浪费MCU时间还容易在调试时把人绕晕。我的做法是先固定TFT横屏然后旋转摄像头模块让它输出的画面朝上和屏幕显示方向匹配避免软件旋转。3.5 DMA刷屏别让CPU等像素SPI刷一帧数据ST7735S 128x16020480个像素RGB565就是40960字节如果SPI是20MHz理论传输时间约16.4ms。但如果你用while循环一个字节一个字节发每个字节还要查询发送完成标志位实际耗时可能是理论值的3倍以上。因此最好用DMA方式发送。HC32L072的DMA配置不复杂把SPI发送缓冲地址设为源地址把要发送的数据放内存然后触发DMA传输。DMA传输完成中断里再置一个标志这样CPU可以在DMA搬运数据的同时去读取FIFO里的下一段数据。不过我这里提醒一句DMA传输过程中源数据内存不能变所以要么分配一块缓冲区要么用行缓冲分两次刷。我的做法是定义一个uint16_t的行缓冲每次读一行像素然后用DMA发送一行。4. 摄像头初始化与FIFO读取从寄存器配置到一帧数据落盘4.1 OV7670关键寄存器配置OV7670寄存器配置算是个体力活但核心只要抓住几个方面输出格式、分辨率、时钟分频、以及同步信号极性。下面是我在项目里实际用到的关键配置思路。分辨率设置COM7寄存器将输出分辨率改为QVGA 320x240。这里还要注意OV7670的寄存器很多是银行分组式的改分辨率时可能要同时调整HREF、VTS、HTS等时序寄存器不能只改COM7。输出格式设置RGB565格式。RGB565在OV7670里通常通过COM7和RGB444等寄存器组合配置。像素时钟分频CLKRC寄存器可以控制PCLK频率。使用FIFO模块时PCLK不需要和MCU严格同步所以可以适当降低PCLK减少FIFO写入压力。帧率VSYNC需要作为帧同步信号通常配置成60fps还是30fps取决于寄存器里的VTS_HI和VTS_LO。我们最后要一帧一帧读所以30fps就够。初始化的代码风格大致长这样void ov7670_init(void) { // 复位 ov7670_write_reg(REG_COM7, 0x80); // 软件复位 delay_ms(10); // 设置为QVGA RGB565 ov7670_write_reg(REG_COM7, 0x06); // QVGA, RGB模式 ov7670_write_reg(REG_COM15, 0xC0); // RGB565输出 // 后续还需要根据具体模组配置HSTART/HSTOP等寄存器 }这里我不逐条贴完整的寄存器表因为不同模块差异太大。但你可以在系统里把寄存器配置和屏的初始化序列一样做成一张表驱动方便随时增删。4.2 AL422B FIFO的工作流程AL422B这个FIFO值得一提。它的容量是384KB写入端和读出端各自有独立的时钟和使能。摄像头把像素并行数据通过WCLK写入FIFOMCU通过RCLK和RRST来读出数据。在我们的模块上FIFO通常已经和摄像头接好了摄像头的D7..D0连到FIFO的DIN7..D0摄像头的PCLK连到FIFO的WCLK只要WRST拉低再拉高FIFO写指针复位然后摄像头就开始往FIFO里写。这里有个关键点并不是只要上电摄像头就会不断往FIFO写因为FIFO模块上通常有一个自动写使能机制或者需要通过VSYNC来控制写使能。大多数模块默认是摄像头PCLK一有数据FIFO就写但如果你想只捕获一帧就需要在VSYNC有效后再让摄像头开始写然后等一帧写完后禁用写使能。我画一下我实现的状态机思路等待VSYNC下降沿代表新一帧开始。拉低FIFO WRST复位写指针。拉高FIFO WRST摄像头PCLK开始把本帧数据写入FIFO。等待VSYNC下降沿再次到来说明一帧已经写完或者用定时器估算时间。之后禁止FIFO继续写入具体取决于模块有的模块没有独立写使能那就在读的时候停下PCLK或者用WRST保持复位状态。需要注意的是AL422B的读指针和写指针是独立的。读指针复位需要RRST先低后高。每次读取一帧前都要先RRST然后才开始读否则指针会停留在上一次读取结束的位置画面就会错位。4.3 MCU端读取一帧并送屏由于MCU的SRAM可能不够缓存整帧我采用“边读边刷”的方式读取FIFO的一行像素然后把这一行像素通过SPI DMA发送到TFT屏幕。代码逻辑大致是void display_one_frame(void) { // 等待一帧写入FIFO完成 wait_vsync(); // 复位FIFO读指针 fifo_rrst_low(); delay_us(1); fifo_rrst_high(); // 在TFT上设置显示窗口 tft_set_window(0, 0, DISPLAY_WIDTH - 1, DISPLAY_HEIGHT - 1); // 逐行读取并刷新 for (int y 0; y DISPLAY_HEIGHT; y) { for (int x 0; x DISPLAY_WIDTH; x) { uint16_t pixel read_fifo_pixel(); row_buffer[x] pixel; } tft_send_pixels(row_buffer, DISPLAY_WIDTH); } }这里read_fifo_pixel需要精确控制FIFO的RCLK先拉高读取并行数据再拉低生成一次读时钟。如果你的MCU能做到8位并行读取那一次可以拿到8位数据再读一次拿到高8位拼成一个16位像素。要注意的是OV7670输出RGB565时FIFO里存的是两个字节所以读取顺序是第一个字节可能是高字节第二个是低字节。具体顺序取决于OV7670的RGB565输出格式配置以及FIFO模块的接线。这里可以用软件灵活调整输出到屏幕之前把两个字节交换就行。4.4 摄像头时钟的注意事项OV7670的XCLK输入需要外部时钟源一般接MCU的定时器输出或晶振。XCLK典型值是12MHz到24MHz。我用HC32L072的定时器产生一个方波通过TO引脚输出接摄像头的XCLK。频率不太需要精确但最好稳定在12MHz左右因为OV7670内部的分频和PCLK都是基于XCLK的。如果XCLK波动过大图像会出现横条纹。另外如果模块上已经有晶振那你就不用额外提供XCLK。但我的模块没有所以这一点要特别提醒。5. 联调中的花屏、移位与闪屏问题排查全过程5.1 第一版上电屏幕全白先别改代码我遇到过很多次新板子上电屏幕白得发亮。第一反应千万不要去翻代码而是先量电源。TFT背光需要的电流很大如果LDO选小了背光一开电压就被拉垮。我那次就是背光供电引脚和逻辑供电引脚共用同一路3.3V屏幕一开3.3V掉到2.7VMCU处于半复位状态SPI通信自然失败。后来给背光单独供电问题立刻消失。所以我把这条放在第一位TFT的VCC和背光驱动尽量分开或者至少保证电源余量足够。白屏排查顺序应该是电压、复位引脚电平、SPI波形、初始化时序最后才是代码逻辑。5.2 画面彩虹条纹SPI极性不对如果屏幕能显示内容但出现类似彩虹一样的彩色条纹或者整个画面像被涂抹过那大概率是SPI Mode不匹配。ST7735S和ILI9341支持Mode 0和Mode 3但如果你初始化用的是Mode 0后续刷屏时不小心把SPI设置成了Mode 3就会出现数据采样窗口错误最终显示的颜色完全错乱。还有一点很多MCU的SPI外设支持在传输过程中动态修改CPOL/CPHA但修改之后可能不会立刻生效需要重新禁用SPI再使能。我记得用HC32L072时如果只改寄存器而没有重新enableSPI行为会很怪异。所以在初始化SPI时就把模式固定下来后面不要随便改。5.3 颜色不对R和B交换最经典的一张调试截图是画面里的红色变成蓝色蓝色变成红色整个图像像是用了反色滤镜。出现这种现象原因就是RGB565的字节序反了。排查方法很简单先在屏幕上画一个纯红色块如果TFT显示出来是蓝色说明TFT端或MCU端的颜色打包方式有误。这里不是摄像头的问题因为纯色块是直接写入屏幕的绕过摄像头。修好TFT颜色之后再接入摄像头如果颜色又反了那就是摄像头输出字节序不对。两个环节最好分开验证。5.4 图像上下颠倒或者左右镜像这个多半是摄像头安装方向或者TFT扫描方向导致的。你可以改TFT的坐标映射方向也可以调整OV7670寄存器中的镜像和翻转配置还可以把摄像头模块旋转180度。我个人建议物理调整优先因为软件翻转会额外增加CPU开销在这个Demo里不划算。5.5 图像错位、出现斜切条纹FIFO读指针没有复位如果图像像是被剪切成好几段每一段之间有明显错位最常见的原因是读取前没有正确复位FIFO读指针。FIFO的读指针如果不复位上一帧读到一半的位置会继续被读走下一帧的数据就从错误位置开始。所以每次读取一帧前RRST都要执行一次完整的低-高脉冲而且低电平持续时间要满足AL422B手册要求我用的是至少400ns。另外如果在读取过程中没有正确控制OE输出使能数据总线上会同时出现多个数据源竞争也会导致错位。OE通常拉低即可只有在需要释放总线时才拉高。5.6 屏幕闪烁和撕裂帧率与背光的博弈屏幕闪烁有两个常见来源。一是刷新帧率过低比如一帧刷新耗时80ms人眼已经能明显看到刷新过程这种属于性能上限只能通过减小分辨率或提升SPI速率来解决二是背光PWM频率太低比如用几百Hz的PWM调光人眼能感知到频闪。区分方法很简单把背光亮度调到最大如果还闪那就是刷新帧率问题如果最大亮度不闪、半亮时闪那基本是背光PWM问题。撕裂感则是因为屏幕在刷新过程中FIFO还没完全读完一帧下一帧又开始了导致同一画面里上半部分是旧数据、下半部分是新数据。解决思路是“等一帧写完后马上读”并且读取过程中不再响应新的VSYNC或者在硬件上把FIFO写使能锁住直到整帧读完再释放。我在状态机里加了一个busy标志读帧期间忽略VSYNC中断撕裂问题就消失了。6. 帧率瓶颈与优化思路这个Demo还能怎么改6.1 量化当前性能一帧到底要多久我用的TFT是128x160 ST7735S一帧RGB565数据量为128160240960字节。SPI时钟跑到20MHz理论上传输需要约16.4ms。但实际操作中每一行都需要设置窗口地址这里要注意如果连续刷整屏不需要每行都设置窗口只需要在一开始设置一次完整窗口然后连续发送所有像素数据。否则每行都发CASET、RASET开销会占掉30%以上。按照我的实现读取一行像素需要128次FIFO读操作每次读两个字节需要多次GPIO翻转这部分很耗时。实测下来整帧显示时间大概在50ms左右即约20fps。但这个数字还不够理想因为MCU在等待SPI DMA完成时没有做其他事浪费了带宽。6.2 优化一DMA双缓冲让SPI和FIFO读取并行默认代码是读一行像素放进行缓冲DMA发送这一行CPU等待DMA完成再读下一行。这样SPI发送时CPU是空闲的而FIFO读取时SPI是空闲的两者串行。改进方式是使用两个行缓冲CPU往bufferA里读像素同时SPI DMA正在发送bufferB。等两者都完成后交换缓冲。这样SPI和FIFO读取能部分重叠整体帧率能提升10%~20%。不过这个优化需要DMA中断和主循环之间的配合要小心缓冲区覆盖问题。6.3 优化二降低分辨率从QVGA到QQVGA如果显示目标不需要320x240全分辨率可以把OV7670输出改为160x120QQVGATFT也开一个160x120的窗口只显示中间部分。那数据量降到原来的四分之一即使SPI时钟不变帧率也能翻倍。这个在“测试模式”下非常实用先看整体画面再切到高分辨率细看。6.4 优化三局部刷新TFT屏测试工装上很多时候画面变化范围并不大比如产线上只关心摄像头画面中心的某个区域。这时候可以只把窗口设置在感兴趣的区域只刷新那一小块。ILI9341的窗口刷新指令天然支持这种操作ST7735S也支持。当然这只适用于特定场景如果要求全屏视频级预览这个优化帮不上什么忙。6.5 后续还能扩展什么这个Camera demo project for testing TFT preview已经打通了“采集-缓存-显示”的完整链路后续想加功能就方便了比如加一个按键切换颜色格式在RGB565和灰度之间切换用来看图像处理算法或者加一个定时截屏把当前帧写入SD卡如果想做更高帧率的预览可以换用带DVP接口的MCU或者改用并口TFT屏。就我个人经验来说这类Demo最重要的不是跑得多快而是保留足够的调试接口。我在代码里留了几个全局变量用来显示当前帧时间、FIFO读次数、DMA发送次数这样后面优化性能时能直接量化效果不用靠猜。最后再分享一个小技巧如果你在调试过程中觉得屏幕上总有若有若无的横纹试着把摄像头的XCLK和TFT的SPI时钟错开比如XCLK用12MHzSPI用18MHz不要让它们成整数倍关系。两个时钟之间如果产生差频干扰会在图像上形成周期性条纹错开频率往往立竿见影。
返回列表