使用微雪摄像头,一定不要忘了给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文件内容。