事先声明:本人纯小白,若内容有误还望各位大佬指正。
以下两篇文章提供了很大的帮助,文章涉及部分代码修改自这两篇文章,各位可以参考一下
本文采用的是stm32h723zgt6,用keil5基于hal库编写相关程序(cubemx还不会用),ov7670为淘宝购买的无fifo版本(当时买的时候看着便宜)。图像显示为1.54寸lcd显示屏,lcd显示程序为商家提供的。
回到正题,接下来说说相关配置
3.3v和GND显然一个接电源一个接地。SCL、SDA如果查过相关资料可以发现是用于sccb通信的两根线,程序中我分别配置到了GPIOB的PIN_6和GPIOB的PIN_5,然后嘛,下面是sccb的代码
mysccb.c:
#include "my_sccb.h"
void SCCB_delay(uint8_t n)//延时函数
{
uint32_t i = 0;
for (i = 0; i < (183*n); i++){
__NOP();
};
}
void SetGPIO_Pin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, uint8_t state) {
if (state == 0) {
HAL_GPIO_WritePin(GPIOx, GPIO_Pin, GPIO_PIN_RESET); // 设置低电平
} else {
HAL_GPIO_WritePin(GPIOx, GPIO_Pin, GPIO_PIN_SET); //设置高电平
}
}
//写scl
void SCCB_W_SCL(uint8_t BitValue)
{
SetGPIO_Pin(GPIOB,GPIO_PIN_6,BitValue);
SCCB_delay(50);
}
//写sda
void SCCB_W_SDA(uint8_t BitValue)
{
SetGPIO_Pin(GPIOB,GPIO_PIN_5,BitValue);
SCCB_delay(50);
}
//读sda
uint8_t SCCB_R_SDA(void)
{
uint8_t BitValue;
BitValue = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_5);
SCCB_delay(50);
return BitValue;
}
//SCCB初始化
void SCCB_Init(void)
{
__HAL_RCC_GPIOB_CLK_ENABLE();
//SCL 在pin6、SDA 在pin5
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStructure.Pin = GPIO_PIN_6|GPIO_PIN_5;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
// PWDN、reset引脚配置
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStructure.Pull = GPIO_NOPULL;
GPIO_InitStructure.Pin = GPIO_PIN_1 | GPIO_PIN_2;
HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
// RESET引脚置1 ,PWDN引脚置 0
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_RESET);
//SDA、SCL置1
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5 | GPIO_PIN_6, GPIO_PIN_SET);
}
//SDA输入配置
void SCCB_SDA_IN(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.Mode = GPIO_MODE_INPUT;
GPIO_InitStructure.Pull = GPIO_PULLUP;
GPIO_InitStructure.Pin = GPIO_PIN_5 ;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_SET);
}
//SDA输出配置
void SCCB_SDA_OUT(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStructure.Pin = GPIO_PIN_5 ;
GPIO_InitStructure.Pull = GPIO_NOPULL;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_SET);
}
// sccb时序开始
void SCCB_Start(void)
{
SCCB_W_SDA(1);
SCCB_W_SCL(1);
SCCB_W_SDA(0);
SCCB_W_SCL(0);
}
//sccb停止
void SCCB_Stop(void)
{
SCCB_W_SDA(0);
SCCB_W_SCL(1);
SCCB_W_SDA(1);
}
//SCCB发送1字节
void SCCB_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i ++)
{
SCCB_W_SDA(Byte & (0x80 >> i));
SCCB_W_SCL(1);
SCCB_W_SCL(0);
}
}
//SCCB接收1字节
uint8_t SCCB_ReceiveByte(void)
{
uint8_t i, Byte = 0x00;
SCCB_SDA_IN();
for (i = 0; i < 8; i ++)
{
SCCB_W_SCL(1);
if (SCCB_R_SDA() == 1){Byte |= (0x80 >> i);}
SCCB_W_SCL(0);
}
SCCB_SDA_OUT();
return Byte;
}
//SCCB发送NA信号
void SCCB_SendNA(void)
{
SCCB_W_SDA(1);
SCCB_W_SCL(1);
SCCB_W_SCL(0);
SCCB_W_SDA(0);
}
//SCCB接收应答,如果没接收从机的应答lcd会弹出error
uint8_t SCCB_ReceiveAck(void)
{
uint8_t AckBit;
SCCB_SDA_IN();
SCCB_W_SCL(1);
AckBit = SCCB_R_SDA();
SCCB_W_SCL(0);
SCCB_SDA_OUT();
if (AckBit){
LCD_Clear();
LCD_SetColor(LCD_WHITE);
LCD_DisplayString(1,1,"error");
//HAL_Delay(1000);
return 0xFF;
}
return AckBit;
}
这里可以看到程序中SDA引脚在接收与输出之间存在切换,具体原因可以移步到文章开头提到的第二篇参考文章。代码中还配置了PWDN和RET,ret对应复位,这里给高电平,对应GPIO_PIN_1;PWDN则配置到了GPIO_PIN_2,给低电平。
mysccb.h
#ifndef __SCCB_H
#define __SCCB_H
#include "main.h"
#include "stdint.h"
#include "lcd_spi_154.h"
void SCCB_delay(uint8_t n);
void SetGPIO_Pin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, uint8_t state);
void SCCB_W_SCL(uint8_t BitValue);
void SCCB_W_SDA(uint8_t BitValue);
uint8_t SCCB_R_SDA(void);
void SCCB_Init(void);
void SCCB_Start(void);
void SCCB_Stop(void);
void SCCB_SendByte(uint8_t Byte);
uint8_t SCCB_ReceiveByte(void);
void SCCB_SendNA(void);
uint8_t SCCB_ReceiveAck(void);
void SCCB_SDA_OUT(void);
void SCCB_SDA_IN(void);
#endif
lcs_spi为1.54寸lcd显示的头文件
HS和VS为帧同步信号和行同步信号,PLK为像素时钟,XLK为输入的时钟信号,因此除XLK外都需要单片机接收,这里我配置HS在GPIOB的PIN_3,HS在GPIOB的PIN_4(也就是HREF?我也不太清楚但至少能用),PLK在GPIOB的PIN_7,XLK在GPIOB的PIN_0。然后就是剩下的D0-D7:这几个为输出图像的数据,图像输出的格式为RGB565(我自己是这么配置的,其它格式还有RGB555等),由于为RGB565格式,两个字节就能表示一个像素点的RGB值,而D0-D7正好8个引脚,一次输出1个字节,两次就能输出1个像素了。D0-D7设置在GPIOA的PIN_0~PIN7
ov7670.c
#include "ov7670.h"
#include "string.h"
//0100 0010---读地址(0x42) 0100 0011---写地址(0x43)
#define OV7670_ADDRESS 0x42
uint8_t OV7670_VS(void)
{
return HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_3);
}
uint8_t OV7670_HREF(void)
{
return HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_4);
}
uint8_t OV7670_PCLK(void)
{
return HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_7);
}
//OV7670引脚初始化
void OV7670_Pin_Init()
{
// VS HREF 和 PCLK IO 配置
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.Pin=GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_7; // PB3,PB4,PB7
GPIO_InitStruct.Mode=GPIO_MODE_INPUT; //上拉输入
GPIO_InitStruct.Pull=GPIO_PULLUP;
GPIO_InitStruct.Speed=GPIO_SPEED_FREQ_HIGH ;
HAL_GPIO_Init(GPIOB,&GPIO_InitStruct);
//D0-D7 IO口设置在GPIOA
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin=GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7;
GPIO_InitStruct.Mode=GPIO_MODE_INPUT; //上拉输入
GPIO_InitStruct.Pull=GPIO_PULLUP;
GPIO_InitStruct.Speed=GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA,&GPIO_InitStruct);
}
// OV7670Ð写寄存器
void OV7670_WriteReg(uint8_t RegAddress, uint8_t Data)
{
SCCB_Start();
SCCB_SendByte(OV7670_ADDRESS);
SCCB_ReceiveAck(); //SCCB接收应答
SCCB_SendByte(RegAddress);
SCCB_ReceiveAck();
SCCB_SendByte(Data);
SCCB_ReceiveAck();
SCCB_Stop();
}
//OV7670读寄存器
uint8_t OV7670_ReadReg(uint8_t RegAddress)
{
uint8_t Data;
SCCB_Start();
SCCB_SendByte(OV7670_ADDRESS);
SCCB_ReceiveAck();
SCCB_SendByte(RegAddress);
SCCB_ReceiveAck();
SCCB_Stop();
SCCB_Start();
SCCB_SendByte(OV7670_ADDRESS | 0x01);
SCCB_ReceiveAck();
Data = SCCB_ReceiveByte();
SCCB_SendNA();
SCCB_Stop();
return Data;
}
//寄存器配置
void OV7670_Configure(void)
{
LCD_SetBackColor(LCD_BLACK);
LCD_Clear();
OV7670_WriteReg(0x12, 0x80);//寄存器重置
HAL_Delay(1000);
//rgb通道增益
OV7670_WriteReg(0x00, 0x00);
OV7670_WriteReg(0x01, 0x00);
OV7670_WriteReg(0x02, 0x00);
//睡眠模式关闭
OV7670_WriteReg(0x09, 0x00);
//com3
OV7670_WriteReg(0x0c, 0x00);
//com4
OV7670_WriteReg(0x0d, 0x00);
//xclk分频,分频系数为后6位+1
OV7670_WriteReg(0x11, 0x05);
//QCIF(176*144)+rgb输出格式
OV7670_WriteReg(0x12, 0x0c);
//OV7670_WriteReg(0x12, 0x14);
//com8自动增益控制、白平衡、曝光
OV7670_WriteReg(0x13, 0xFF);
//行同步信号延迟
OV7670_WriteReg(0x1b, 0x00);
OV7670_WriteReg(0x30, 0x00);
OV7670_WriteReg(0x31, 0x00);
//上下左右镜像
OV7670_WriteReg(0x1e,0x30);
//adc控制
OV7670_WriteReg(0x20, 0x04);
OV7670_WriteReg(0x3a, 0x00);
//夜晚模式,最高位使能
OV7670_WriteReg(0x3b, 0x00);
//不知道
OV7670_WriteReg(0x3e, 0x00);
//边缘增强
OV7670_WriteReg(0x3f, 0x00);
//rgb565格式
OV7670_WriteReg(0x40,0xd0);
//com16
OV7670_WriteReg(0x41, 0x08);
//com17
OV7670_WriteReg(0x42, 0x00);
//输入时钟倍频
OV7670_WriteReg(0x6b, 0x40);
//关闭测试图像
OV7670_WriteReg(0x70, 0x00);
OV7670_WriteReg(0x71, 0x00);
//去噪声偏移
OV7670_WriteReg(0x77, 0xFF);
//黑白矫正
OV7670_WriteReg(0x77, 0xA0);
HAL_Delay(1000);
}
// OV7670初始化
void OV7670_Init(void)
{
LCD_Clear();
LCD_SetColor(LCD_WHITE);
SCCB_Init();//SCCB初始化
OV7670_Pin_Init();
OV7670_Configure();/
LCD_Clear();
LCD_DisplayString(0, 0,"all Init done");
HAL_Delay(1000);
LCD_Clear();
LCD_DisplayNumber(1,1,OV7670_ReadReg(0x0a),4);//读设备id,理论上为76(10进制就是118)
HAL_Delay(1000);
}
uint16_t frame[176 * 144]; //用于存储图像
void OV7670_GetPic(void)
{
uint16_t i,j;
memset(frame, 0, sizeof frame);
//下面两行我给注释掉了,因为实际测试过程加这两行程序就卡死了
//while(OV7670_VS()==0);//保证新的帧时序,而不是在帧时序的一半进入
//while(OV7670_VS()==1);
for(i=0;i<176;i++)
{
while(OV7670_HREF()==0);
for(j=0;j<144;j++)
{
uint16_t pixel = 0x0000;
//读取rgb565
while(OV7670_PCLK()==0);
pixel |= (GPIOA->IDR & 0xFF)<< 8;
while(OV7670_PCLK()==1);
while(OV7670_PCLK() == 0);
pixel |= (GPIOA->IDR & 0xFF);
while(OV7670_PCLK() == 1);
frame[144 * i + j] = pixel;
}
if(i==175){break;}
while(OV7670_HREF()==1);
}
while(OV7670_VS()==0);
}
程序说明:本人最终输出图像是上下两半严重错位的,可能在最后一个获取图像的循环函数有问题
ov7670.h
#ifndef __OV7670_H
#define __OV7670_H
#include "my_sccb.h"
#include "lcd_spi_154.h"
#include "mypwm.h"
extern uint16_t frame[176 * 144];
uint8_t OV7670_VS(void);
uint8_t OV7670_HREF(void);
uint8_t OV7670_PCLK(void);
void OV7670_Init(void);
void OV7670_GetPic(void);
void OV7670_WriteReg(uint8_t RegAddress, uint8_t Data);
uint8_t OV7670_ReadReg(uint8_t RegAddress);
#endif
这里frame定义为全局的,方便主函数直接读取
最后,最重要的一个,前面提到XLK为时钟信号,而PLK的频率就是根据前面寄存器配置中的分频系数和倍频计算得出,PLK的频率为24MHZ/[5(分频系数)+1]*[4(倍频)]=16MHZ,倍频系数看寄存器输入的最高2位:00 不倍频,01为4倍频,10为6倍频,11为8倍频。个人实测PLK频率12MHZ和18MHZ为宜(这个自己尝试吧)
那么pwm哪里来,这个就需要再配置一个引脚作为pwm输出提供给XLK了,这里我设置的是GPIOB_PIN_0,对应定时器TIM3的3号通道(这个查数据手册吧)
mypwm.c:
#include "mypwm.h"
void TIM3_PWM_Init(void)
{
__HAL_RCC_TIM3_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.Pin = GPIO_PIN_0;
GPIO_InitStructure.Mode = GPIO_MODE_AF_PP;
GPIO_InitStructure.Pull = GPIO_NOPULL;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStructure.Alternate = GPIO_AF2_TIM3;
HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
// config tim3
TIM_HandleTypeDef TimHandle;
TimHandle.Instance = TIM3;
TimHandle.Init.Prescaler = 0; // 预分频
TimHandle.Init.CounterMode = TIM_COUNTERMODE_UP;
TimHandle.Init.Period = 10; // 自动重装
TimHandle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_PWM_Init(&TimHandle);
// config pwm channel
TIM_OC_InitTypeDef sConfig;
sConfig.OCMode = TIM_OCMODE_PWM1;
sConfig.Pulse = 5; // 占空比50%
sConfig.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfig.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&TimHandle, &sConfig, TIM_CHANNEL_3);
HAL_TIM_PWM_Start(&TimHandle, TIM_CHANNEL_3);
}
我的频率是275MHZ, 通过11分频,输出pwm频率就是25MHZ了,约等于24MHZ
mypwm.h
#ifndef __MYPWM_H
#define __MYPWM_H
#include "stm32h7xx_hal.h"
#include "stm32h7xx_hal_tim.h"
void TIM3_PWM_Init(void);
#endif
但是嘛编译以下,报错,定时器这些函数未定义,但我明明导入库了啊,一番查资料,在这篇文章中给出了答案:参考文章3。原来HAL_TIM_MODULE_ENABLED这行被注释掉了,删掉注释,一个编译,然后就完成了
然后就是main函数,把这几个头文件include就行了
main.c
int main(void)
{
SCB_EnableICache();
SCB_EnableDCache();
HAL_Init();
SystemClock_Config();
LED_Init();
USART1_Init();
SPI_LCD_Init();
LCD_Clear();
LCD_SetColor(LCD_WHITE);
TIM3_PWM_Init();
OV7670_Init();
LCD_Clear();
while (1)
{
OV7670_GetPic();
RGB565DRAW(10,10,176,144);
}
}
这里就展示主函数了,前面一些初始化的来自商家给的lcd程序,主函数就一个获取图像一个绘图
main.c里我还定义了一个画图的函数(其实就遍历frame把点一个一个显示上去)
void RGB565DRAW(uint16_t x,uint16_t y,uint16_t width,uint16_t height)
{
for(uint16_t i=0;i<width;i++)
{
for(uint16_t j=0;j<height;j++)
{
LCD_DrawPoint(x+i,y+j,frame[height*i+j]);
}
}
}
LCD_DrawPoint(x+i,y+j,frame[height*i+j]);为商家提供在在lcd指定xy坐标点显示特定的颜色,当然main.h里别忘了声明这个函数
最后看看效果吧,前面提到了错位图片中很明显的,代码肯定存在一定问题,还望各位多多理解
顺便记录,pwm程序有没有效的方法以及pwm频率的测量
由于没有示波器,这频率显然测不出,正好手头有一块小的stm32烧录着以前刚学单片机那会写的测pwm频率的程序
对,就是这种。但是显然这种频率下这单片机也不好测,但我能改pwm的频率啊。于是把分频系数和自动重装值都给调大,占空比保持50%,这样频率马上就能降下来,测量也就轻轻松松了。再通过测量的频率以及自动重装值还有分频系数就能反推出时钟频率,设计一个特定频率的pwm就轻轻松松了。
还有一种检测方法,拿个万用表测量pwm的电压,改变pwm占空比就能影响测得的电压了
最后嘛,由于没开DMA,速度肯定嘎嘎慢,从输出结果来看程序一定是有点问题的,如果有啥问题或者错误还望各位指正,第一次写博客,表述若有不当还望多多包涵