OV2640 Camera module on STM32 (without hardware DCMI interface)


This article is very old and uses an outdated ChibiOS HAL. It is kept here for reference only.

OV2640 is a quite old camera chip/module annouced in 2005 and it is available for few USD from eBay nowadays. One of its main features (compared to other common cheap modules) is the JPEG output.

It uses a parallel camera interface which can be easily connected to a STM32 DCMI peripheral and read using DMA. However, only the high performance STM32 families have such peripheral. I was discovering some possibilities of connecting this module to the STM32 without the DCMI peripheral and came with a working solution (which is not innovative in any way, it is just a right combination of other peripherals on the STM32).

The information provided below is not meant to be complete and you will not find anything ready to be flashed to your board. Also, no arduino/AVR supported.

Some code snippets use ChibiOS HAL API, some don’t. It shouldn’t be hard to convert those to use different library/API.

==== Hardware Connections ====

  • PWDN - camera power down signal (active high). This signal can be left unconnected - it has an internal pull-down resistor according to datasheet. It can be connected to any STM32 GPIO pin in push-pull output mode.

  • RES - camera reset, it can also be left unconnected. Active low. Internal pull-up. There are multiple diagrams in the datasheet showing how to use the power down mode and how a correct camera reset and initialization should look like. It can be connected to any STM32 GPIO pin in push-pull output mode.

  • XCLK - main camera clock input. It has to be connected to a clock source of 24MHz typ. (6MHz minimum). STM32 MCO clock output can be used for this purpose. I used a timer in PWM mode (50% duty) because I had the MCO pin used for other purpose. The clock input is not critical, it can be really anything from 6MHz and up.

  • DATA D0-D7 - data output from the camera. Actually it has a 10 bit output marked Y0-Y9 in the datasheet. On the module, there are D7-D0 markings (corresponding to Y9-Y2) and two additional pins Y1 and Y0. Only D7-D0 are used, JPEG data are 8bit wide. You can probably set different alignment and shift modes. I used defaults (see the datasheet). These 8 pins must be connected to one half of a GPIO port for the DMA to work (either low or high 8 bits).

  • HREF - horizontal reference output. It marks when data on D0-D7 pins is valid. It must be connected to a GPIO input connected to an EXTI channel.

  • VSYNC - vertical sync output. It is used to mark a single JPEG frame boundaries. It must be connected to a GPIO input connected to an EXTI channel (a different one than HREF)

  • PCLK - pixel clock output. It is used as a strobe for D0-D7 data. Data is valid on its rising edge. It must be connected to a STM32 timer input capture channel - it is used to trigger the DMA.

  • SIO_C/SIO_D - two pins of a I2C-compatible bus called SCCB in the datasheet. I2C bus is used for camera configuration. It needs two pull-up resistors, internal STM32 pull-ups can be used if the speed is kept low.

![Hardware connection](img/hw_connection.svg)

==== Software - camera interface ====

=== Initialize GPIO pins ===

Pin configuration, ChibiOS HAL is used:


/* XCLK, timer 3, channel 1, AF2 / palSetPadMode(GPIOC, 6, PAL_MODE_ALTERNATE(2)); / Reset / palSetPadMode(GPIOC, 8, PAL_MODE_OUTPUT_PUSHPULL); / PWDN */ palSetPadMode(GPIOC, 9, PAL_MODE_OUTPUT_PUSHPULL);

/* PCLK, timer1 input capture, channel 1 */ palSetPadMode(GPIOA, 8, PAL_MODE_ALTERNATE(1)); </code>

Other pins are configured as inputs by default. PORTE pins 8-15 are used for data input (they are connected to the D0-D7 outputs of the camera).

=== Initialize I2C to communicate with the camera using SCCB bus ===

I2C1 is configured with 100kHz clock and internal pullups. No problems were observed even with 20cm wires. Used configuration was

<code c> static const I2CConfig i2cfg1 = {


}; </code>

Initialize it the usual way:

<code c> i2cStart(&I2CD1, &i2cfg1); </code>

SCCB registers can be set by writing two bytes (register address and value) over the I2C using an address 0x60 >> 1. Reading is similar - write register address and then read its value back.

=== Reset and initialize the camera ===

From hardware app notes it can be seen that the initialization sequence is:

  • initial state is all signals low, SCCB is pulled high, no power

  • apply power to camera

  • apply clock input

  • wait at least 3ms

  • set RES to H and wait at least 2ms

  • configure the camera using SCCB interface

  • power down input must be low

Initialization using ChibiOS HAL:

<code c> /* apply clock here */ pwmStart(&PWMD3, &pwmcfg3); pwmEnableChannel(&PWMD3, 0, 1); chThdSleepMilliseconds(5);

/* reset the camera */ palClearPad(GPIOC, 8); chThdSleepMilliseconds(5); palSetPad(GPIOC, 8); chThdSleepMilliseconds(5);

/* set PWDN low to exit power down */ palClearPad(GPIOC, 9); chThdSleepMilliseconds(10); </code>

=== Configure DMA and timer input capture DMA trigger

We capture the JPEG data using a DMA reading from the GPIO peripheral (GPIOE IDR register) and triggering it using a timer input capture. DMA configuration in circular mode is below. Note that the DMA is not started yet. It will be started on the frame beginning (marked with VSYNC going high):

<code c> uint32_t dmamode; const stm32_dma_stream_t *dmastp; uint8_t sample_buffer[60000];

/* … */

dmastp = STM32_DMA_STREAM(STM32_DMA_STREAM_ID(2, 3)); dmamode = STM32_DMA_CR_CHSEL(6) |


dmaStreamAllocate(dmastp, 2, (stm32_dmaisr_t)dma_interrupt, NULL);

dmaStreamSetPeripheral(dmastp, (uint8_t *)(&(GPIOE->IDR)) + 1); dmaStreamSetMemory0(dmastp, sample_buffer); dmaStreamSetTransactionSize(dmastp, sizeof(sample_buffer));

dmaStreamSetMode(dmastp, dmamode); </code>

Timer 1 input capture is also configured, DMA trigger will be configured later:

<code c> TIM_TypeDef *tim = (TIM_TypeDef *)STM32_TIM1; rccEnableTIM1(FALSE); rccResetTIM1(); tim->CCR1 = 0; tim->CCER = 0; tim->ARR = 10; tim->CNT = 0; tim->SR = 0;

/* set channel 1 to input / tim->CCMR1 = TIM_CCMR1_CC1S_0; / enable first input capture channel */ tim->CCER = TIM_CCER_CC1E;

tim->CR2 = 0; tim->CR1 = 0; </code>

### Configure EXTI for HREF and VSYNC inputs

Now we need to enable EXTI for HREF and VSYNC output. HREF is connected to EXTI channel 1 (PA1 input), VSYNC is connected to channel 2 (PB2 input). These two routines are used to turn on/off DMA and timer input capture according to HREF/VSYNC rising/falling edges and signal a semaphore when the captured JPEG frame is ready.




We are using two EXTI channels with their corresponding interrupt service routines:

<code c> /* HREF EXTI interrupt handler (channel 1) */ CH_IRQ_HANDLER(EXTI1_IRQHandler) {


if (palReadPad(GPIOA, 1)) {

/* HREF rising edge, start capturing data on pixel clock */ STM32_TIM1->DIER = TIM_DIER_CC1DE;

} else {

/* HREF falling edge, stop capturing */ STM32_TIM1->DIER = 0;



} </code>

<code c> /* VSYNC EXTI interrupt handler (channel 2) */ CH_IRQ_HANDLER(EXTI2_IRQHandler) {


if (palReadPad(GPIOB, 2)) {
/* VSYNC rising edge - frame is starting, start DMA and enable
  • timer 1 input capture trigger. LED showing that capture is

  • running is connected to PD12. */

dma_start(); /* we also enable input capture before HREF goes high - otherwise

  • we may miss some bytes at the beginning og the frame. This also

  • adds more invalid bytes which will need to be removed later */

STM32_TIM1->DIER = TIM_DIER_CC1DE; palSetPad(GPIOD, 12);

} else {
/* VSYNC falling edge - end if JPEG frame. Stop the DMA, disable
  • capture LED and signal the semaphore (data can be processed). */

dma_stop(); palClearPad(GPIOD, 12);

chSysLockFromIsr(); chSemSignalI(&frame_ready_sem); chSysUnlockFromIsr();



} </code>

dma_start() and dma_stop() routines just manipulate the DMA EN bit.

<code c> int32_t dma_start(void) {

(dmastp)->stream->CR |= (STM32_DMA_CR_EN); return 0;


int32_t dma_stop(void) {

(dmastp)->stream->CR &= ~(STM32_DMA_CR_EN); return 0;

} </code>

You can see that we are using the DMA circular buffer mode but we stop the dma on transfer complete. There are two things you can do:

  • if you have enough SRAM to store the whole frame, just turn off the circular mode and save your data on DMA transfer complete (or simply just disable the DMA when the transfer is complete - VSYNC goes low).

  • if you have some space to store your frames which is fast enough (ie. it can save the frame faster than the camera outputs it), you can use a small SRAM buffer and use the DMA circular mode. In this case you need to signal the semaphore on half transfer and transfer complete interrupt and process both buffer halves continuously.

After the DMA transfer is complete, we need to remove any leading garbage bytes from the beggining of the frame until we get 0xff 0xd8 (JPEG starts with these two bytes). This is a consequence of starting the timer input capture prematurely to avoid missing bytes.

==== Sample source code ====

Sources are available at https://github.com/iqyx/ov2640-stm32. It is a proof-of-concept implementation.

Datasheets for the OV2640 camera module and other documents can be found at https://github.com/iqyx/ov2640-stm32/tree/master/doc