K60智能车 —— 摄像头和DMA的配置、传输到上位机显示、OLED屏幕上显示

虽然摄像头容易受光干扰、信息处理起来稍显复杂,不如电磁那样简单和稳定,但是因为摄像头前瞻相对较远并且可调,因此速度上限更高。下面简单介绍摄像头ov7725的配置

一、摄像头与DMA

1.先简单了解一下ov7725,采用的是SCCB协议,关于此的介绍参考这篇进行了解:https://blog.csdn.net/weixin_43529046/article/details/90453815

2.另外我参考野火关于ov7725的介绍来梳理一下内容。用的是野火鹰眼摄像头,是硬件二值化摄像头即只有黑白两色,场中断是一帧图像的开始,场中断出现后既会触发自身的场中断函数(主要负责交换缓冲区数组以及设置一些标志位),也会打开DMA传输(因为DMA的中断请求源设为了场中断引脚),然后DMA会在PCLK下降沿采集和传输数据,当采集完一帧图像后就会触发DMA的中断函数(这个中断函数的作用大致也是交换图像缓冲数组)
K60智能车 —— 摄像头和DMA的配置、传输到上位机显示、OLED屏幕上显示_第1张图片这一页介绍了数字摄像头的像素采集顺序即从左到右,从上到下。至于奇偶场,奇数场只采集奇数行的像素值,偶数场只采集偶数行的像素值,因此一帧图的采集需要两场,而ov7725不分奇偶场
K60智能车 —— 摄像头和DMA的配置、传输到上位机显示、OLED屏幕上显示_第2张图片
K60智能车 —— 摄像头和DMA的配置、传输到上位机显示、OLED屏幕上显示_第3张图片提供的几种采集图像的思路
K60智能车 —— 摄像头和DMA的配置、传输到上位机显示、OLED屏幕上显示_第4张图片
一次传输8个像素,一个位就表示一个像素(因为通过了硬件二值化),这样一来,一个字节就表示8
个像素点,一次采集就相当于采了8个像素点。而对于非硬件二值化的,一次采集8位构成了一个字节,这一个字节表示一个像素点,对比之下,硬件二值化的采集速度是它的8倍。硬件二值化由此也带来了一个问题,对于每一个像素点的访问比较麻烦,因为像素是和位对应起来的,因此就需要解压函数对其格式进行修改,把每一位都单独取出来,重新放到数组中以方便访问,这也就是后面解压函数的作用

3.摄像头配置

原理图
K60智能车 —— 摄像头和DMA的配置、传输到上位机显示、OLED屏幕上显示_第5张图片K60智能车 —— 摄像头和DMA的配置、传输到上位机显示、OLED屏幕上显示_第6张图片对照着看,我们需要配置的有数据线、时钟信号线、场中断信号线(没有奇偶场,一场就是一帧)、SCCB控制信号线

3.1 GPIO口的配置

//数据口的配置
static GPIO_InitTypeDef data_init;
data_init.GPIO_PTx = PTC;//引脚组
data_init.GPIO_Dir = DIR_INPUT;//方向
data_init.GPIO_Pins = GPIO_Pin8_15;//引脚号
data_init.GPIO_PinControl = IRQC_DIS | INPUT_PULL_DIS;
LPLD_GPIO_Init(data_init);

//场信号口的配置
static GPIO_InitTypeDef vsyn_init;
vsyn_init.GPIO_PTx = PTC;
vsyn_init.GPIO_Dir = DIR_INPUT;
vsyn_init.GPIO_Pins = GPIO_Pin19;
vsyn_init.GPIO_PinControl = IRQC_RI|INPUT_PULL_DOWN|INPUT_PF_EN ;
vsyn_init.GPIO_Isr = vsyn_isr;//触发场中断后调用场中断函数
LPLD_GPIO_Init(vsyn_init);
LPLD_GPIO_EnableIrq(vsyn_init);

//PCLK信号口配置
static GPIO_InitTypeDef pclk_init;
pclk_init.GPIO_PTx = PTC;
pclk_init.GPIO_Pins = GPIO_Pin18;
pclk_init.GPIO_Dir = DIR_INPUT;
pclk_init.GPIO_PinControl = IRQC_DMAFA | INPUT_PULL_DIS;
LPLD_GPIO_Init(pclk_init);

3.2 SCCB接口的配置
在底层库DEV_SCCB.h文件中可以找到SCCB的引脚配置,只需对照原理图修改SCL、SDA对应的引脚即可

//定义引脚
#define SCCB_SCL  		PTC16_O
#define SCCB_SDA_O		PTC17_O
#define SCCB_SDA_I		PTC17_I

//输入输出
#define SCCB_SDA_OUT()	DDRC17=1
#define SCCB_SDA_IN()	DDRC17=0

此外,DEV_SCCB.c文件中的LPLD_SCCB_Init()函数也需要修改引脚

void LPLD_SCCB_Init(void)
{
	GPIO_InitTypedef gpio_init;
	gpio_init.GPIO_Ptx=PTC;//引脚组
	gpio_init.GPIO_Pins=GPIO_Pin16|GPIO_Pin17;//引脚号
	gpio_init.GPIO_Dir=DIR_OUTPUT;
	gpio_init.GPIO_Output=OUTPUT_H;
	gpio_init.GPIO_PinControl=NULL;
	LPLD_GPIO_Init(gpio_init);
}

4 . DMA的配置
参考这篇了解DMA的知识:https://blog.csdn.net/zhejfl/article/details/82555634
K60智能车 —— 摄像头和DMA的配置、传输到上位机显示、OLED屏幕上显示_第7张图片K60智能车 —— 摄像头和DMA的配置、传输到上位机显示、OLED屏幕上显示_第8张图片
展示了部分请求源

#define ROW		120
#define COLUMN	160

//图像缓存区,都是8位的,恰好对应
uint8	Image_Buf[ROW+10][COLUMN/8];//[130][20]
uint8	Image_Buf2[ROW+10][COLUMN/8];

static DMA_InitTypeDef dma_init_struct;
dma_init_struct.DMA_CHx = DMA_CH0;//CH0通道
dma_init_struct.DMA_Req = PORTC_DMAREQ;//PORTC为请求源
dma_init_struct.DMA_MajorLoopCnt = ROW *COLUMN/8;//主循环计数值:字节数
dma_init_struct.DMA_MinorByteCnt = 1;//次循环字节计数:每次读入1字节(8位)
dma_init_struct.DMA_SourceAddr = (uint32)&PTC->PDIR+1;// 数据的源地址:PTC0~7
dma_init_struct.DMA_DestAddr = (uint32)Image_Buf;//配置目的数据地址目的地址:存放图像的数组
dma_init_struct.DMA_DestAddrOffset = 1;//目的地址偏移:每次读入增加1
dma_init_struct.DMA_MajorCompleteIntEnable = TRUE;//完成中断请求
dma_init_struct.DMA_AutoDisableReq =TRUE;
dma_init_struct.DMA_Isr =DMA_complete_isr;//中断函数
LPLD_DMA_Init(dma_init_struct);
LPLD_DMA_EnableIrq(dma_init_struct); 

以上DMA的配置完成了:
可以从PORTC接收传输请求,然后去读取PRTC0 - 7端口的数据值,次循环每次读一个字节恰好对应8个端口的8位数据即1个字节,按照这样的读法读取 (120 x 160 / 8)次,就算完成了一次的传输,触发DMA中断。另外,传输的数据存放在数组Image_Buf[ ][ ]中,其数组元素为8位的一个字节,地址每次偏移1字节就相当于移动到下一个数组元素。

5 . 摄像头寄存器的初始化

//宏定义标志寄存器地址
#define OV7725_ID        0x21
#define OV7725_GAIN      0x00
#define OV7725_BLUE      0x01
#define OV7725_COM7      0x12
#define OV7725_CNST      0x9C
......//把所有寄存器地址进行宏定义


//自定义结构体类型
typedef struct{
	uint8 addr;//寄存器地址
	uint8 val;//寄存器的值
}reg_s;
//定义一个此类型的结构体数组变量,即每个成员都是一个结构体,构成了一个数组
reg_s ov7725_eagle_reg[]=
{
	{OV7725_COM4,0xC1},
	{OV7725_CLKRC,0x00},
	{OV7725_COM2,0x03},
	{OV7725_COM3,0xD0},
	{OV7725_COM7,0x40},
	......
	//把摄像头所需的寄存器地址以及值都放在这里,方便使用
	//OV7725_COM4表示的是寄存器地址,用的是宏定义
}

uint8 Camera_CNST=60;
uint8 ov7725init()
{
	int16 i=0,j=0,device_id=0;
	LPLD_SCCB_Init();//DEV_SCCB.c文件中定义的初始化函数
	camera_delay();//延时函数
	while(0==LPLD_SCCB_WriteReg(OV7725_COM7,0x80))
	{//复位:把值0x80写入到寄存器OV7725_COM7中,把所有寄存器恢复到默认值
		i++;
		if(i==500)
			return 0;//长时间等待时则退出
	}
	//初始化数组
	for(i=0;i<120;i++)
	{
		for(j=0;j<160;j++)
		{
			Image[i][j]=0;
		}
	}
	//初始化缓冲数组
	for(i=0;i<120;i++)
	{
		for(j=0;j<20;j++)
		{
			Image_Buf[i][j]=255;
			Image_Buf2[i][j]=255;
		}
	}
	camera_delay();
	//读取设备的ID号,确定设备存在
	if(0==LPLD_SCCB_ReadReg(OV7725_VER,&devive_id,1))
	//参数:寄存器地址,读出来后存到这个地址,读取长度
	{
		return 0;//读取失败则退出
	}
	camera_delay();
	if(device_id==OV7725_ID)//唯一的地址对应上
	{
		for(i=0;i<ov7725_eagle_cfgnum;i++)
		{
			if(0==LPLD_SCCB_WriteReg(ov7725_eagle_reg[i].addr,ov7725_eagle_reg[i].val))
			//把寄存器值按照寄存器地址逐个写入到寄存器中,从而实现对摄像头的配置
			{
				return 0;//出现写入失败就退出
			}
		}
		LPLD_SCCB_WriteReg(OV7725_CNST,Camera_CNST);
	}
	else
	{
		return 0;
	}
	return 1;
	
}

关于ov7725寄存器的设置,会影响到其帧率、大小等,参考这篇了解:https://www.cnblogs.com/raymon-tec/p/5090185.html

通过把摄像头各个寄存器配置成想要的值从而实现不同的帧率、亮度、对比度等以满足不同的需求,这也就是摄像头寄存器初始化的主要内容。需要修改的寄存器有,PCLK速率,帧率、图像亮度、对比度、色饱和度等

6 . 场中断函数
每一场结束即每一帧结束就触发场中断函数

//主要是在修改DMA的目的地址,因为有两个缓冲区交替使用
void vsyn_isr()
{
	static int8 i =0;
	static uint8 *p;
	if(LPLD_GPIO_IsPinxExt(PORTC,GPIO_Pin19))
	//检测引脚是否产生了外部中断即
	{
		if(i<5) i++;
		if(i>=3)
		{
		//Image_complete_flag和WhichBuffer的值由是否产生了DMA中断来控制
			if(Image_complete_flag==1)
			{
				Image_complete_flag=0;
				i=0;
				LPLD_GPIO_ClearIntFlag(PORTC);
				//两个缓冲区,交替使用
				if(WhichBuffer==1)
				{
					p=(uint8*)Image_Buf2;
					LPLD_DMA_LoadDstAddr(DMA_CH0,p);//加载地址到DMA
				}
				else
				{
					p=(uint8*)Image_Buf;
					LPLD_DMA_LoadDstAddr(DMA_CH0,p);//加载地址到DMA
				}
				LPLD_DMA_EnableReq(DMA_CH0);//使能DMA中断
			}
		}
	}
	LPLD_GPIO_ClearIntFlag(PORTC);
}

7 . DMA中断函数
完成一帧图像的传输就会触发一次DMA中断,从而通知CPU可以进行读取图像数据了,并且使用另一个图像缓冲数组来存图像数据。因为图像不能边传边读,只能先把一帧图完全采集完毕并传过来,CPU才能进行读取这帧图像,因此有了两个图像缓冲区,当Buffer1用于接收DMA传输的图像,CPU可以去解压读取Buffer2中已经是完整的图像数据了。当Buffer1存满图像数据,则CPU可以去解压读取Buffer1中的数据,而把已经解压读取过的Buffer2用作接收DMA的图像数据。实现交替使用,一边在传输,一边在解压读取图像

int8 WhichBuffer=0,Sample_over=0;
int8 Image_complete_flag=1;//标志位

void DMA_complete_isr()
{
	Sample_over=1;//采样结束
	Image_complete_flag=1;
	if(WhichBuffer==1)
	{
		WhichBuffer=0;
	}
	else
		WhichBuffer=1;

}

8 . 解压图像

像素介绍:
单色:一个像素只占1位,因此只能存储黑白信息;
16色位图:一个像素占4位,有16种颜色可选
256色位图:一个像素占8位即1个字节,有256种颜色可选
24位位图:RGB图像,一个像素占24位即3个字节,每个分量占8位,有2^24种颜色可选

uint8 Image_Buf[ROW + 10][COLUMN / 8];//每个元素是8位一个字节
uint8 Image_Buf2[ROW + 10][COLUMN / 8];//每个元素是8位一个字节
unsigned char Image[ROW][COLUMN];//缓冲区数组拆分出来的位存到这里,一个位表示一个像素
//解压图像,进行格式转化
void image_exact()
{
	uint8 color[2]={255,0};//二值化,只有黑白两色
	uint8 Which;
	Which=WhichBuffer;
	if(Which==1)
	{
		for(i=0;i<120;i++)
		{
			for(j=0;j<COLUMN/8;j++)// COLUMN/8 = 20
			{
				Image[i][j*8+0]=color[(Image_Buf[i][j]>>7) & 0x01];//取出最高位,对应此字节的第7个像素点
				Image[i][j*8+1]=color[(Image_Buf[i][j]>>6) & 0x01];//取出次高位,对应此字节的第6个像素点
				Image[i][j*8+2]=color[(Image_Buf[i][j]>>5) & 0x01];//取出第5位,对应此字节的第5个像素点
				Image[i][j*8+3]=color[(Image_Buf[i][j]>>4) & 0x01];//取出第4位,对应此字节的第4个像素点
				Image[i][j*8+4]=color[(Image_Buf[i][j]>>3) & 0x01];//取出第3位,对应此字节的第3个像素点
				Image[i][j*8+5]=color[(Image_Buf[i][j]>>2) & 0x01];//取出第2位,对应此字节的第2个像素点
				Image[i][j*8+6]=color[(Image_Buf[i][j]>>1) & 0x01];//取出第1位,对应此字节的第1个像素点
				Image[i][j*8+7]=color[(Image_Buf[i][j]>>0) & 0x01];//取出第0位,对应此字节的第0个像素点
			}//实现了把每一位都取出来重新存放到Image[120][160]数组中
		}
	}
	else
	{
		for(i=0;i<120;i++)
		{
			for(j=0;j<COLUMN/8;j++)
			{
				Image[i][j*8+0]=color[(Image_Buf2[i][j]>>7) & 0x01];
				Image[i][j*8+1]=color[(Image_Buf2[i][j]>>6) & 0x01];
				Image[i][j*8+2]=color[(Image_Buf2[i][j]>>5) & 0x01];
				Image[i][j*8+3]=color[(Image_Buf2[i][j]>>4) & 0x01];
				Image[i][j*8+4]=color[(Image_Buf2[i][j]>>3) & 0x01];
				Image[i][j*8+5]=color[(Image_Buf2[i][j]>>2) & 0x01];
				Image[i][j*8+6]=color[(Image_Buf2[i][j]>>1) & 0x01];
				Image[i][j*8+7]=color[(Image_Buf2[i][j]>>0) & 0x01];
			}
		}
	}
}

需要明白解压的目的是什么:
DMA传过来的数据是Image_Buf数组或者Image_Buf2数组,即120行,20列的数据(因为1位就表示了一个像素点,因此所占的字节数没那么多,就只有20列,对应160位,160个像素),而为了便于访问,需要把这些位都展开即变成160列的。解压只是把数据存储形式改变,方便访问,点数还是那么多。比如,如果第0行的字节为0x01,0x02,0x01,0x02,0x01,0x02,0x01,0x02 … 共20个字节,刚好160位,对应160个像素,填满了一行,但如果具体想要访问每个像素点,需要对位进行访问,十分麻烦,因此,期望展开成:00000001,00000010,00000001,00000010,00000001,00000010,00000001,00000010 …
填充到具体的数组中则是:Imag[0][0]=0, Imag[0][1]=0, Imag[0][2]=0, Imag[0][3]=0, Imag[0][4]=0, Imag[0][5]=0, Imag[0][6]=0, Imag[0][7]=1(这是第1个字节对应的黑点),Imag[0][0]=0, Imag[0][1]=0,
Imag[0][2]=0, Imag[0][3]=0, Imag[0][4]=0, Imag[0][5]=0, Imag[0][6]=1(这是第2个字节对应的黑点),Imag[0][7]=0 … 这一黑色点的值是1还是255,不影响颜色的判断

解压的作用就是把每一位都取出来重新存放以方便访问

二、蓝牙传输到上位机

主要是通过UART通信的方式传到上位机,主要介绍一下UART。UART需要事先约定一个波特率即传输速率:每秒传递的位数,以及是否有校验位等。然后,接收方也就可以按照这个约定的速率去读取传来的数据从而进行正确的解读,并且每一字节的传输都有起始位和停止位,方便识别。参考这篇了解UART:https://wenku.baidu.com/view/a3b76f4ae43a580216fc700abb68a98270feac27.html
K60智能车 —— 摄像头和DMA的配置、传输到上位机显示、OLED屏幕上显示_第9张图片UART的配置(像电机那样,没有GPIO的配置初始化,可以尝试一下对其进行GPIO初始化会不会有影响,进而判断GPIO的配置对哪些外设是必要的,哪些不是必要的)

static UART_InitTypeDef UART_InitStructure;
UART_InitStructure.UART_Uartx = UART0;//模块号选择
UART_InitStructure.UART_BaudRate = 115200//波特率
UART_InitStructure.UART_RxPin = PTA1;  //接收引脚
UART_InitStructure.UART_TxPin = PTA2;  //发送引脚
UART_InitStructure.UART_RxIntEnable = TRUE; //使能接收中断
UART_InitStructure.UART_RxIsr = UART_recive_isr; //接收中断回调函数,一旦接收到数据就会触发此中断
LPLD_UART_Init(UART_InitStructure);
LPLD_UART_EnableIrq(UART_InitStructure);

发送的时候不必触发中断,因此只需调用库函数发送数据即可,一次发送一个8位的一字节。由于要发送到上位机,因此需要带有帧头帧尾。可以在中断中调用这个发送函数从而实现定时发送数据

void uart_image()
{
	int i,j;
	uint8 temp;
	LPLD_UART_PutChar(UART0, 0x01);
	LPLD_UART_PutChar(UART0, 0xFE);//这两句发送了帧头
	for (i = 0; i < ROW; i+=2)//跳着发?
  	{
    	for (j = 0; j < COLUMN; j+=2)
    	{
      		temp = 0;
     		temp = Image[i][j]; //
      
      		LPLD_UART_PutChar(UART0,temp);//库函数,一次发送一个8位一字节
    	}
  	}
  
  	LPLD_UART_PutChar(UART0, 0xFE);
  	LPLD_UART_PutChar(UART0, 0x01);//这两句发送了帧尾
	
}

如果一个MCU通过蓝牙接收另一个设备发送来的数据,需要通过UART的接收中断函数来实现接收

//自定义结构体类型
typedef struct
{
	int Stack;
	uint8 Data;
	uint8 Buffer[4];//4个字符构成的字符串
} SerialPortType;
//声明一个这样结构的结构体变量
SerialPortType SerialPortRx;//串口接收端

void UART_recive_isr()
{
 	//UART_S1_RDRF_MASK是接收寄存器对应的掩码
	//if(UART0->S1 & UART_S1_RDRF_MASK)  表示接收数据寄存器满
	//if(UART0->C2 & UART_C2_RIE_MASK ) 表示发送数据寄存器空
	if ((UART0->S1 & UART_S1_RDRF_MASK) && (UART0->C2 & UART_C2_RIE_MASK))
	{
		SerialPortRx.Data = LPLD_UART_GetChar(UART0);//从UART0读取一个数据
		SerialPortRx.Buffer[SerialPortRx.Stack] = SerialPortRx.Data;//存储到数组中,方便后续连成字符串
	}
}

三、在OLED屏幕上显示

1 . 在OLED屏幕上显示

显示原理:
对于128 x 64像素的OLED显示屏,也就是说显示屏横向有128个像素点,纵向有64个像素点,共128 x 64=8192个像素点,该像素点有亮(1)和灭(0)两种状态,由此构成不同的图像。每个点是否亮由寄存器中的值决定,我们的任务就是告诉它哪个点该亮,哪个该灭。每一纵行的64个像素点被分成8个页面,共8 x 128个页面,每个页面有8个像素点,其亮灭状态以8个位即1字节的形式存储在一个寄存器中,数据中的每一位对应一个像素点,相应的就有8 x 128个8位寄存器。实际使用过程中,把要显示的图片通过取模软件就能转成数组,把这个数组的数据写入到OLED对应的寄存器中就能达到显示图像的目的

显示字符:
字符的显示原理和图片是一样的,只不过为了美观和显示方便,设置每个字符的宽和高是一样的,宽高为6 x 8像素,也就是6个字节;每个汉字的宽高为14 x 16像素,即28个字节

1.2 此OLED屏幕的接口,是SPI

SPI双向传输至少需要4根线,单向传输只需3根线:
SDI(主设备输入,从设备输出),SDO(主设备输出,从设备输入),SCLK(时钟信号,由主设备产生),CS(从设备使能信号,由主设备控制),其中CS控制芯片是否被选中,也即是说只有片选信号为预先规定的使能信号(高电位或低电位),对此芯片的操作才有效,这就使得在同一总线上连接多个SPI设备成为了可能

由SCLK提供时钟脉冲,SDI、SDO则基于此脉冲完成数据的传输,数据输出通过SDO线,数据在时钟上升沿或下降沿时改变,在紧接着的下降沿或上升沿被读取,完成1位的数据传输。因此至少需要8次时钟信号的改变(上升沿和下降沿为1次)才能完成8位数据的传输。但缺少应答机制


K60智能车 —— 摄像头和DMA的配置、传输到上位机显示、OLED屏幕上显示_第10张图片
K60智能车 —— 摄像头和DMA的配置、传输到上位机显示、OLED屏幕上显示_第11张图片VCC:5V电源输入;GND:接地端;

DC:数据、命令选择器,HIGH表示输入的是数据,LOW表示输入的是命令;

RES:复位引脚,当RES为LOW时进行初始化,在使用OLED的过程应保持RES为HIGH

SCL:时钟信号输入;

SDA:数据或命令输入

//宏定义其引脚和简单函数功能
#define OLED_GPIO_SCLK 	GPIO_Pin15//时钟
#define OLED_GPIO_SDA 	GPIO_Pin16//数据
#define OLED_GPIO_RST 	GPIO_Pin13//复位
#define OLED_GPIO_DC	GPIO_Pin17

//控制引脚的输出,方便实现模拟协议
#define OLED_SCLK(x) LPLD_GPIO_Output_b(PTA, 15, x)
#define OLED_SDA(x)  LPLD_GPIO_Output_b(PTA, 16, x)
#define OLED_RST(x)  LPLD_GPIO_Output_b(PTA, 13, x)
#define OLED_DC(x)   LPLD_GPIO_Output_b(PTA, 17, x)

1.3 OLED首先需要配置GPIO口

//对应的四线引脚
static GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_PTx = PTA;
GPIO_InitStructure.GPIO_Pins =  OLED_GPIO_SCLK|OLED_GPIO_SDA | OLED_GPIO_RST | OLED_GPIO_DC;//引脚
GPIO_InitStructure.GPIO_Dir = DIR_OUTPUT;
GPIO_InitStructure.GPIO_Output = OUTPUT_H;
LPLD_GPIO_Init(GPIO_InitStructure);

1.4 然后进行接口初始化的配置,需要写很多子函数,不过应该都有现成的吧

	OLED_DC(0);//拉低后表示输入的是命令
	OLED_SDA(0);
	OLED_SCLK(0);
	OLED_RST(0);//RES处于低,表示在初始化状态
	OLED_DelayMs(50);
        
	OLED_SCLK(1);//SCL拉高
	OLED_RST(0);//RES处于低,表示在初始化状态
	OLED_DelayMs(50);//等待初始化
	OLED_RST(1);//OLED使用过程中需保持RES为高
	
	//常规操作配置(子函数)
	SetDisplay_On_Off(0x00);	// Display Off (0x00/0x01)
	SetDisplayClock(0x80);		// Set Clock as 100 Frames/Sec
	SetMultiplexRatio(0x3F);	// 1/64 Duty (0x0F~0x3F)
	SetDisplayOffset(0x00);		// Shift Mapping RAM Counter (0x00~0x3F)
	SetStartLine(0x00);			// Set Mapping RAM Display Start Line (0x00~0x3F)
	SetChargePump(0x04);		// Enable Embedded DC/DC Converter (0x00/0x04)
	SetAddressingMode(0x02);	// Set Page Addressing Mode (0x00/0x01/0x02)
	SetSegmentRemap(0x01);		// Set SEG/Column Mapping     0x00左右反置 0x01正常
	SetCommonRemap(0x08);		// Set COM/Row Scan Direction 0x00上下反置 0x08正常
	SetCommonConfig(0x10);		// Set Sequential Configuration (0x00/0x10)
	SetContrastControl(0xCF);	// Set SEG Output Current
	SetPrechargePeriod(0xF1);	// Set Pre-Charge as 15 Clocks & Discharge as 1 Clock
	SetVCOMH(0x40);				// Set VCOM Deselect Level
	SetEntireDisplay(0x00);		// Disable Entire Display On (0x00/0x01)
	SetInverseDisplay(0x00);	// Disable Inverse Display On (0x00/0x01)
	SetDisplay_On_Off(0x01);	// Display On (0x00/0x01)
	OLED_Fill(0x00);
	OLED_SetPosition(0,0);


1.5 如何把图像显示到OLED屏幕上

显示图像,输入的是数据而不是命令

//向OLED写入命令的子函数功能
void OLED_WriteCmd(unsigned char cmd)
{
	unsigned char i = 8;

	OLED_DC(0);//处于LOW表示写入的是命令
	OLED_SCLK(0);

	while(i--)
	{
		if(cmd & 0x80)
			OLED_SDA(1);
		else
			OLED_SDA(0);

		OLED_SCLK(1);
		asm("nop");
		OLED_SCLK(0);//模拟SPI的时序图

		cmd <<= 1;
	}
}

//向OLED写入数据字符
void OLED_WriteData(unsigned char data)
{
	unsigned char i = 8;

	OLED_DC(1);//HIGH表示写入的是数据
	OLED_SCLK(0);

	while(i--)
	{
		if(data & 0x80)
			OLED_SDA(1);
		else
			OLED_SDA(0);

		OLED_SCLK(1);
		asm("nop");
		OLED_SCLK(0);

		data <<= 1;
	}
}

//把图像写入到OLED屏幕上
void Showimage()
{
  int16 i,j,cnt,temp1,data;
  for (j = 0; j < 8; j++)
  {
    OLED_WriteCmd(0xb0 + j);
    OLED_WriteCmd(0x01);
    OLED_WriteCmd(0x10);//写入命令
    for (i = 0; i < 80; i++)
    {
      temp1 = 0;
      for ( cnt = 7; cnt >= 0; cnt--)
      {
        temp1 |= Image[(j * 8 + cnt) * 2][i * 2] / 253;
        temp1 = temp1 << 1;
      }
      temp1 |= Image[(j * 8) * 2][i * 2] / 253;
      data = temp1;
      OLED_WriteData(data);//写入数据
    }
  }
}

总结一下:

1.摄像头:ov7725,SCCB协议,硬件二值化使得采集速度更快,需要GPIO口(数据口、场中断、PCLK口)的配置,SCCB接口的配置,另外还需要摄像头寄存器的配置从而实现不同的对比度、亮度等,摄像头与DMA的配合,解压图像以方便访问每一个像素点

2.DMA配置:选好模块号、请求源、主次循环、源地址和目的地址,还有DMA中断,注意有两个图像缓冲数组

3.蓝牙通信:注意蓝牙接收中断的使用,用于接收其他设备通过蓝牙传过来的数据

4.OLED屏幕:采用SPI接口,显示原理和显示方式

在复习总结的过程中,发现自己还是有不少地方理解的不清晰,还需要后续继续学习和应用。做车的时候对这方面关注的不够多,多集中在控制程序和算法的编写,但这些外设和模块的配置是这个智能车比赛能学到的重要的一部分,不能因为前人对外设配置的比较好了而忽略这方面的学习。

对于刚接触智能车的,如果有例程,可能会急于让车动起来而忽略这些外设的配置,顺利的话车能动起来,要是不顺利的话可能会很打击人的自信,因为我自己零基础入门这个智能车的时候经历了太多的波折,如果对例程的结构和配置没有基本的掌握会很容易陷入问题的漩涡之中。我建议,最好先找一些先关方面的资料,对k60的相关模块有一个大致的了解,然后结合例程和LPLD底层库文档去研究例程,理清结构,尽量明白每一步配置所起到的作用,确保你们的硬件配置不要出现太多的问题,然后才能尽可能专心的研究控制程序和算法。这个比赛无论你有没有走到国赛的赛场,它都是一个很好的学习k60的机会。拿起板子,编写自己的程序,理解其中的原理。

另外,因为个人水平有限,上面总结的地方可能有些地方不清晰甚至有错误,还请大家能够指出来。写总结一方面是加深自己的理解,另一方面是希望能够帮助做车的人入门,不要像我一样走太多的弯路而消磨意志。

参考资料:

  1. 清华大学工程物理系学生科协技术部智能车大赛技术文档
  2. ov7725数字摄像头编程基本知识笔记

你可能感兴趣的:(智能车,嵌入式,c语言,经验分享)