【程序+PCB】STM32F107VC单片机利用外部中断和DMA获取OV2640摄像头拍摄的照片,并通过串口发送到电脑上(HAL+LL库版)

使用微雪摄像头,一定不要忘了给XCLK提供24MHz的时钟!!!!否则I2C无响应!!!
另外,用F1单片机的话,I2C必须要有上拉电阻!!!

本程序由STM32F103RE单片机利用外部中断和DMA获取OV2640摄像头拍摄的照片,并通过串口发送到电脑上修改而来,在STM32F107VC单片机上运行。程序修改成了基于STM32CubeF1 HAL+LL库。(详情请参阅原文)
摄像头用的是微雪的OV2640摄像头模块。

程序下载地址:https://pan.baidu.com/s/1wucAsyguxdXy6FzNE7g0aQ(提取码:r5ut)
原理图和PCB文件下载地址:https://pan.baidu.com/s/19C_SHKWcu_5t5N2SQobsVQ(提取码:627e)

【电路】

F1单片机必须在I2C上外接上拉电阻,其他系列不需要。

【关于微雪摄像头的RET和PWDN引脚】

微雪OV2640摄像头的RET和PWDN引脚默认情况下是没有接到摄像头上的,无法使用,这是因为背面有两个0R电阻没有焊。建议把这两个0R电阻焊上去(用焊锡直接短路也行),使RET和PWDN两个引脚可用。否则摄像头一旦死机,读写不了寄存器了,就只能板子重新上电才能恢复了。板子上最好在RESET引脚上接一个下拉电阻,保证没有XCLK时钟的时候OV2640处于复位状态。

笔者程序里面只用到了RET引脚,所以只将RET用焊锡短接了。

【板子存在的不足】

1. 板子采用12V电源供电,由LM2596S-5.0降压到5V。但是输入端没有任何保护电路,如果频繁插拔电,LM2596S会有烧毁的可能。
笔者昨天晚上就因插拔电太快烧毁了一个,花了将近一个小时把LM2596S芯片拆下来,换上新的就好了。
笔者认为输入端加一个瞬态抑制二极管(TVS管),以及一个大的滤波电容,会好一些。

2. 在板子通电的情况下,插上摄像头,单片机复位后,有可能会出现能读到ID,但写不了寄存器的情况:
STM32F107VC OV2640
SystemCoreClock=72000000
Manufacturer ID: 0x7fa2, Product ID: 0x2642
Failed to write OV2640 register! addr=0x35, data=0xda

SIGABRT: Abnormal termination
Exited! returncode=1
摄像头复位RET也不起作用,始终读写不了寄存器。只有板子拔掉电源再通电,才能恢复正常。

3. 如果单片机不使用摄像头的RET复位引脚,程序烧进单片机,连续按下复位键几次后,摄像头有可能死机,既读不了ID,也读写不了寄存器,I2C无响应。
只有板子重新上电后,才能恢复正常。
如果单片机使用了RET复位引脚,输出XCLK时钟前拉低RET。那么就能解决连续按复位键摄像头死机的问题。但是不能问题(2)。

4. 板子插着摄像头上电,串口芯片MAX3232有一定概率启动失败,发烫,无法正常使用。(无论是否使能了RET信号)
电压表量2脚电压是2V,6脚电压是0.6V。正常情况下2脚应该是6V,6脚应该是-6V。
板子不插摄像头上电,串口芯片MAX3232就不会出问题。
因此,此摄像头和MAX3232串口芯片不能共存。可以考虑换一个串口芯片,如MAX232。

5. HC-05蓝牙串口插座和右边的电位器距离太近,插不下蓝牙模块。同样,左下角的白色复位按钮和下面的拨动开关也离得太近了。

6. AMS1117的3.3V走线太细了。板子是完全自动布线的,当时没发现。不排除问题(4)是因为这个造成的。

【主要代码】

#include <stdio.h>
#include <stm32f1xx.h>
#include <string.h>
#include "common.h"
#include "dcmi_ov2640.h"

#define STATE_START 0x01
#define STATE_STOP 0x02

CRC_HandleTypeDef hcrc;
I2C_HandleTypeDef hi2c1;
static uint8_t image_buffer[63716]; // 如果存储空间不够了, 把这个数组改小就行了
static uint8_t image_state; // 图像捕获状态
static uint32_t image_size; // 图像的大小

/* 初始化摄像头 */
static void camera_init(void)
{
  GPIO_InitTypeDef gpio;
  LL_DMA_InitTypeDef dma;
  LL_TIM_InitTypeDef tim;
  LL_TIM_IC_InitTypeDef tim_ic;
  LL_TIM_OC_InitTypeDef tim_oc;
  
  __HAL_RCC_AFIO_CLK_ENABLE();
  __HAL_AFIO_REMAP_I2C1_ENABLE();
  __HAL_AFIO_REMAP_TIM3_ENABLE();
  
  __HAL_RCC_CRC_CLK_ENABLE();
  __HAL_RCC_DMA1_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();
  __HAL_RCC_GPIOC_CLK_ENABLE();
  __HAL_RCC_GPIOE_CLK_ENABLE();
  __HAL_RCC_I2C1_CLK_ENABLE();
  __HAL_RCC_TIM3_CLK_ENABLE();
  
  hcrc.Instance = CRC;
  HAL_CRC_Init(&hcrc);
  
  // PB8~9连接摄像头的I2C接口, 设为复用开漏输出
  gpio.Mode = GPIO_MODE_AF_OD;
  gpio.Pin = GPIO_PIN_8 | GPIO_PIN_9;
  gpio.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(GPIOB, &gpio);
  
  // PC3(VSYNC)和PC6(=~(HREF & PCLK))为浮空输入
  // PE0~7是摄像头的8位数据引脚, 为浮空输入
  
  // PC5为OV2640的复位引脚, 低电平复位
  HAL_GPIO_WritePin(GPIOC, GPIO_PIN_5, GPIO_PIN_RESET);
  gpio.Mode = GPIO_MODE_OUTPUT_PP;
  gpio.Pin = GPIO_PIN_5;
  gpio.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(GPIOC, &gpio);
  
  // PC7(XCLK)是OV2640时钟, 由TIM3_CH2提供
  // 微雪OV2640摄像头的RET和PWDN引脚默认情况下是没有接到摄像头上的, 无法使用, 这是因为背面有两个0R电阻没有焊
  gpio.Mode = GPIO_MODE_AF_PP;
  gpio.Pin = GPIO_PIN_7;
  gpio.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(GPIOC, &gpio);
  // 建议把这两个0R电阻焊上去, 使RET和PWDN两个引脚可用
  // 否则摄像头一旦死机, 读写不了寄存器了, 就只能板子重新上电才能恢复了
  
  LL_TIM_StructInit(&tim);
  tim.Autoreload = 2; // 72MHz/(2+1)=24MHz
  tim.Prescaler = 0; // 不分频
  LL_TIM_Init(TIM3, &tim);
  
  LL_TIM_OC_StructInit(&tim_oc);
  tim_oc.CompareValue = 1; // 决定占空比
  tim_oc.OCMode = LL_TIM_OCMODE_PWM2;
  tim_oc.OCState = LL_TIM_OCSTATE_ENABLE;
  LL_TIM_OC_Init(TIM3, LL_TIM_CHANNEL_CH2, &tim_oc);
  LL_TIM_EnableCounter(TIM3); // 打开定时器, 开始输出24MHz XCLK时钟
  HAL_Delay(10);
  HAL_GPIO_WritePin(GPIOC, GPIO_PIN_5, GPIO_PIN_SET); // 有了XCLK时钟, 才撤销OV2640复位信号
  
  hi2c1.Instance = I2C1;
  hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
  hi2c1.Init.ClockSpeed = 10000; // 速率: 10kHz (不可太高, 否则会导致Ack Failure)
  HAL_I2C_Init(&hi2c1);
  
  // 每当PC6上出现下降沿时, 就发送一次DMA请求, 采集GPIOC低8位的数据
  LL_TIM_IC_StructInit(&tim_ic);
  tim_ic.ICActiveInput = LL_TIM_ACTIVEINPUT_DIRECTTI; // PC6(TIM3_CH1)映射到TIM3_CH1上
  tim_ic.ICPolarity = LL_TIM_IC_POLARITY_FALLING; // 下降沿触发
  LL_TIM_IC_Init(TIM3, LL_TIM_CHANNEL_CH1, &tim_ic);
  // 无需让定时器3开始计时, 这里只使用该定时器的一个输入捕获通道
  
  // 配置TIM3_CH1对应的DMA通道
  dma.Direction = LL_DMA_DIRECTION_PERIPH_TO_MEMORY;
  dma.MemoryOrM2MDstAddress = (uint32_t)image_buffer;
  dma.MemoryOrM2MDstDataSize = LL_DMA_MDATAALIGN_BYTE;
  dma.MemoryOrM2MDstIncMode = LL_DMA_MEMORY_INCREMENT;
  dma.Mode = LL_DMA_MODE_NORMAL;
  dma.NbData = sizeof(image_buffer); // 采集的最大图像大小, 超出部分会被自动丢弃!!
  dma.PeriphOrM2MSrcAddress = (uint32_t)&GPIOE->IDR;
  dma.PeriphOrM2MSrcDataSize = LL_DMA_PDATAALIGN_BYTE;
  dma.PeriphOrM2MSrcIncMode = LL_DMA_PERIPH_NOINCREMENT;
  dma.Priority = LL_DMA_PRIORITY_VERYHIGH;
  LL_DMA_Init(DMA1, LL_DMA_CHANNEL_6, &dma);
  LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_6);
  
  OV2640_Init(JPEG_800x600);
  
  // 打开PC3外部中断
  LL_GPIO_AF_SetEXTISource(LL_GPIO_AF_EXTI_PORTC, LL_GPIO_AF_EXTI_LINE3);
  LL_EXTI_EnableRisingTrig_0_31(LL_EXTI_LINE_3); // PC3上的上升沿能触发中断
  LL_EXTI_EnableFallingTrig_0_31(LL_EXTI_LINE_3); // PC3上的下降沿也能触发中断
  LL_EXTI_EnableIT_0_31(LL_EXTI_LINE_3);
  HAL_NVIC_EnableIRQ(EXTI3_IRQn); // 允许执行中断服务函数
}

/* 向串口发送图像数据, 并在末尾附上CRC校验码 */
static void dump(const void *data, uint32_t size)
{
  uint8_t value;
  uint32_t i;
  uint32_t temp;
  
  __HAL_CRC_DR_RESET(&hcrc);
  for (i = 0; i < size; i++)
  {
    // 输出图像数据
    value = *((uint8_t *)data + i);
    printf("%02X", value);
    
    // 每4字节计算一次CRC
    if ((i & 3) == 0)
    {
      if (i + 4 <= size)
        temp = HAL_CRC_Accumulate(&hcrc, (uint32_t *)((uint8_t *)data + i), 1);
      else
      {
        temp = 0;
        memcpy(&temp, (uint8_t *)data + i, size - i);
        temp = HAL_CRC_Accumulate(&hcrc, &temp, 1);
      }
    }
  }
  
  // 输出CRC
  temp = (temp >> 24) | ((temp >> 8) & 0xff00) | ((temp & 0xff00) << 8) | ((temp & 0x00ff) << 24);
  printf("%08X\n", temp);
}

int main(void)
{
  HAL_Init();
  
  clock_init();
  usart_init(115200);
  
  printf("STM32F107VC OV2640\n");
  printf("SystemCoreClock=%u\n", SystemCoreClock);
  
  camera_init();
  
  while (1)
  {
    if (image_state == (STATE_START | STATE_STOP))
    {
      printf("size=%d\n", image_size);
      dump(image_buffer, image_size); // 通过串口发送图像, 然后附上CRC校验值
      
      // 让DMA内部指针回到数组的开头
      LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_6);
      LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_6, sizeof(image_buffer));
      LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_6);
      
      image_state = 0; // 允许采集新图像 (这条语句一次性把START和STOP都清0了)
    }
  }
}

void EXTI3_IRQHandler(void)
{
  LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_3); // 清除中断标志位
  if (LL_GPIO_IsInputPinSet(GPIOC, LL_GPIO_PIN_3))
  {
    // PC3上升沿表示图像数据传输开始
    if (image_state != 0)
      return; // 如果图像已经开始采集了, 就忽略这个开始信号
    image_state = STATE_START;
    
    // 打开TIM3_CH1对应的DMA通道, 开始采集数据
    LL_TIM_EnableDMAReq_CC1(TIM3); // 允许PC6上的下降沿触发DMA请求
  }
  else
  {
    // PC3下降沿表示图像数据传输结束
    if ((image_state & STATE_START) == 0 || (image_state & STATE_STOP))
      return; // 忽略没有开始信号的结束信号, 以及重复的结束信号
    image_state |= STATE_STOP;
    
    LL_TIM_DisableDMAReq_CC1(TIM3); // 停止采集
    image_size = sizeof(image_buffer) - LL_DMA_GetDataLength(DMA1, LL_DMA_CHANNEL_6); // 总量-剩余数据量=图像大小
  }
}

笔者本来也想把不用DMA的外部中断版本也改成库函数方式的,但是失败了,实测发现会丢字节,无法得到完整的jpg文件内容。

  • 2
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

巨大八爪鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值