I’ve previously got the STM32F0 discovery board working with a FIFO (AL422b) version of the OV7670, not a particularly hard feat and many others have achieved this across the internet. From what I can find however, nobody has managed to interface an OV7670 with an STM32F0 without external memory or a FIFO!
Today however, thats all going to change as I’m here to show you how to do it. It took a few days of mulling over how I was going to do this but the main method goes:
- Connect the OV7670 PCLK to TIM1 IC channel 1
- Use TIM1 IC1 to trigger a DMA transfer from GPIOA to SPI2
- Configure the OV7670 to gate PCLK with HREF and VSYNC
- Configure the ILI9163 for every row before blitting a line
Its surprisingly simple and ends up being about 300 lines of code (including all initialisation).
I do however need to note that I’m running the ILI9163 at 24MHz SPI clock rate – much faster than the specified 15.15MHz but hey, its only for a bit of fun.
Cameras are known for their super high data rates. Lets start by throwing a few numbers around. The OV7670 has a maximum resolution of 640×480 pixels at a colour depth of 16bits meaning each pixel is 2 bytes. This translates to a full frame size of 640*480*2 = 614.4kB. To achieve a frame rate of 30fps, this translates to a data rate of 18.432Mbyte/s! For an STM32F051 running at full speed (48MHz), this might be just about achievable between certain peripherals depending on bus overheads and a few other factors. This is not however feasible for reading data and throwing it at another device, especially when the secondary device is being written to through SPI.
The maximum data rate specified for an ILI9163 LCD is 15.15MHz. This translates to a byte rate of 1.89MByte/s. This LCD however only has a resolution of 160×128 pixels at 16bpp. This is remarkably close to 160×120 pixels – a standard resolution known as QQVGA which is supported natively by the OV7670.
A full QQVGA 16bpp frame is equivalent to 160*120*2 = 38.4kB. With a 24MHz clock, the maximum byte rate achievable (and therefore pixel clock from the OV7670 as half a 16bpp pixel is sent per clock) is 3MByte/s or 3MHz. A 3MHz clock is quite easily achievable as I’m driving the OV7670 with a 12MHz clock generated by TIM2. This however gives zero room for internal delays with regards to TIM1 capturing the input, or the AHB/APB bus being full. I therefore chose a pixel clock of ~2.5MHz (achieved through PLL multiplication of xclk and some division) as this allows the STM32F0 a bit of time to transfer from GPIOA to SPI2 through DMA. It is worth noting that this transfer crosses from an AHB to APB bus so there is probably a reasonable amount of internal latency. With a pixel clock of this rate, I can achieve a frame rate of ~12fps.
To ensure I could capture pixels and only pixels without external hardware, I needed to gate PCLK so it was only active during the visible duration of the frame. There are still a few timing issues here which manifest as jitter and the edges of the LCD not displaying the correct pixels but I can have a go at ironing them out later. The main aim here was to try and get a stable image without any form of external memory!
I achieved this gating through two registers in the OV7670, COM10 (0x15) and COM12 (0x3C).
Required register settings for PCLK gating during inactive video region
By enabling these two register changes, PCLK doesn’t toggle during blanking meaning TIM1 IC1 isn’t triggered and DMA transactions don’t occur. During blanking, the STM32F0 can be used as normal as the DMA won’t be thrashing the memory bus.
As well as these clock gating register changes, others were required for changing the scaling. I used the OV7670 Implementation guide for this as they give video settings for QQVGA (albeit for YUV – I changed the video format to RGB). The actual initialisation settings however were found across the internet though I think they originated in a .c file for Linux…
On the STM32F0 side, the settings are pretty standard. The pins and their functions are:
- PA0-PA7 are inputs connected to D0-D7 of the OV7670, respectively
- PA8 is an AF (TIM1 IC1) connected to the pixel clock (PCLK) of the OV7670
- PB3 is an AF (TIM2 OC2) and connected the input clock (XCLK) of the OV7670.
- PC14 and PC0 are configured as inputs and are connected to VSYNC and HREF on the OV7670, respectively
- PC13 and PC15 are configured as open drain outputs and are connected to 4.7k pullup resistors and SCL and SDA on the OV7670
- PB11 and PB12 are configured as outputs and are connected to CS and AO of my ILI9163 LCD
- PB13 and PB15 are AFs (SPI Clock and SPI data) and are connected to SCK and SDA on the ILI9163 LCD
The internal settings are:
- TIM2 is configured for a period of 1 with a prescaler of 0 and OC2 is configured to toggle i.e. generate a 48e6/(2*2) clock.
- TIM1 is configured for a period of 255 with a prescaler of 0. Input capture is configured to channel 1 with a zero length filter, rising edge, a prescaler of div1 and direct connection. The CC1 DMA stream is also enabled
- The DMA is configured for peripheral destination transfers from GPIOA IDR to SPI2 DR, both configured for bytes. The buffer size is 1 and the transfer mode is circular. Priority is also very high to ensure maximum bus access.
- Systick is enabled at the start but disabled once the camera and LCD have been initialized.
Program flow is pretty simple. The STM32F0 initially waits for the blanking period of a frame. Once this has happened, the DMA is enabled and a for loop from 0 to 120 (row count) is entered. The first thing this for loop does is ensure the SPI bus isn’t busy using a blocking wait. After this, the LCD memory range is set to the current row and the CS pin is reset along with the AO pin being set. The STM32F0 then waits for HREF to go high. Once high, it then waits for HREF to go low indicating a full line has been captured. The for loop then returns to the top. Upon all 120 rows being captured, the STM32F0 then waits until VSYNC goes high to indicate blanking time. Once high, the STM32F0 is free to do other tasks until the next frame.
I originally tried setting the LCD memory range to the full 160×120 pixels but this caused really weird pixel shifting as if to indicate each line isn’t the correct length (160 pixels).
Weird pixel shifting/tearing when trying to write a full frame vs individual lines.
Using the single row write method seems to work though there are colour issues and a bit of dodgy timing at the edges of the frame.
Image taken of the LCD and what the camera is viewing. Excuse the angles!
Now that I’ve seen that it is possible to drive an LCD with an STM32F0 intermediary and an OV7670, this will hopefully give me scope for a few more projects! I could always swap the LCD for a 23LC1024 Serial SRAM and do some image editing using the OV7670 and the STM32F0 if I wanted to. Infact, this was my original idea!
For reference, CLKRC (0x11) can be lowered and the SPI Baudrate changed to 12MHz to ensure all devices are ran in spec 🙂 required values will need to be calculated/oscilloscoped though! From what I gather, a 12MHz SPI clock will need a maximum of 1.5MHz on the pixel clock.
Code for this project is pretty messy but can be found on my Github.