【STM32调试(一)】串口发送像素,上位机解析显示。

一、思路

STM32采集OV数据,数据尺寸是QVGA(320*240),RGB565数据格式。采集的FIFO数据是一个像素,占两个字节。每采集一个像素就向串口发送一个像素。上位机是一个串口助手,接收串口数据,将一个RGB565格式像素解析为RGB55格式并显示在上位机。

二、STM32采集数据发送

2.1、OV7725模组

我们使用正点原子的例程进行修改,在接线时注意将数据线绑在一起,其它线绑在一起,以防发生数据干扰。

我买的OV7725摄像头是带FIFO的,因为 OV7725 的像素时钟(PCLK)最高可达 24Mhz,我们用STM32F103的IO口直接抓取,是非常困难的,也十分占耗 CPU,所以我们并不是采取直接抓取来自 OV7725 的数据,而是通过 FIFO 读取,ALIENTEK-OV7725 摄像头模块自带了一个 FIFO 芯片(AL422B),用于暂存图像数据,OV将图像帧存储在FIFO中,CPU就可以自己慢慢读取FIFO中的数据帧,这样就可以很方便的获取图像数据了,而不再需要单片机具有高速 IO,也不会耗费多少 CPU,任意一款MCU都可控制该模块和获取图像。

在这里插入图片描述
1. 串行摄像头控制总线(SCCB)

ATK-OV7725 摄像头模块的所有配置,都是通过 SCCB 总线来实现的。

它由两条数据线组成:一个是用于传输时钟信号的 SIO_C(即 OV_SCL),另一个适用于传输数据信号的 SIO_D(即 OV_SDA)。 SCCB 的传输协议与 IIC 协议极其相似,只不过 IIC 在每传输完一个字节后,接收数据的一方要发送一位的确认数据,而 SCCB 一次要传输 9 位数据,前 8 位为有用数据,而第 9 位数据在写周期中是 don’t care 位(即不必关心位),在读周期中是 NA 位。 SCCB 定义数据传输的基本单元为相( phase),即一个相传输一个字节数据。

2. 驱动程序

中断程序:

PA15位中断输入,接OV7725的VSYNC脚,负责帧同步。
ov_sta是OV的中断标记,状态变量ov_sta初始为0,VSYNC中断到来时,OV开始输出一帧图像,复位FIFO写指针,允许写入FIFO,ov_sta自增至1。

//中断服务函数
u8 ov_sta;
void EXTI15_10_IRQHandler(void)
{			
 
	if(EXTI_GetITStatus(EXTI_Line15)==SET)
	{     
		if(ov_sta<2)
		{
			if(ov_sta==0)
			{
				OV7670_WRST=0;	 	//复位写指针		  		 
				OV7670_WRST=1;	
				OV7670_WREN=1;		//允许写入FIFO
			}else OV7670_WREN=0;	//禁止写入FIFO 	 
			ov_sta++;
		}
	}
	EXTI_ClearITPendingBit(EXTI_Line15);    //清除LINE15上的中断标志位  
}
//外部中断初始化程序
//初始化PA15为中断输入.
void EXTI15_Init(void)
{
		
	GPIO_InitTypeDef GPIO_InitStructure;
    EXTI_InitTypeDef EXTI_InitStructure;
 	NVIC_InitTypeDef NVIC_InitStructure;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO|RCC_APB2Periph_GPIOA,ENABLE);//外部中断,需要使能AFIO和GPIOA时钟

	GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);	//关闭JTAG,使能SWD

	GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_15;//PA15
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置成上拉输入
 	GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA15
	
	GPIO_SetBits(GPIOA,GPIO_Pin_15);
	 
	
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource15);
	
	EXTI_InitStructure.EXTI_Line=EXTI_Line15;
  	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;	
  	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;//下降沿触发
  	EXTI_InitStructure.EXTI_LineCmd = ENABLE;
  	EXTI_Init(&EXTI_InitStructure);	 	//根据EXTI_InitStruct中指定的参数初始化外设EXTI寄存器
	
    NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;			//使能按键所在的外部中断通道
  	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;	//抢占优先级2, 
  	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01;					//子优先级1
  	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;								//使能外部中断通道
  	NVIC_Init(&NVIC_InitStructure); 
    
}

刷新显示:

camera_refresh()负责读取FIFI图像数据,并送至LCD显示。在这里是连续读取的,读完一帧图像就立即将ov_sta置0。若想只读取一帧图像,可以不把ov_sta置0,不进行下一次FIFO读取。这也是做摄像机和照相机的区别。

2.2、串口发送

有两种发送方式:高低位单独发送;合并一起发送。
发送内容:黑白,二值,彩图

选择黑白发送或二值图像发送就会简单许多,而且数据量至少会小一半。但是为了后续上位机有更高质量的图片做处理,这里还是选择发送彩色图像,将采集的像素点直接发送给串口。

采集一个像素,发送一个像素:

for(i=0;i<OV7725_WINDOW_HEIGHT;i++)
{
		for(j=0;j<OV7725_WINDOW_WIDTH;j++)
		{
				GPIOB->CRL=0X88888888;
				OV7725_RCK=0;
				color=OV7725_DATA;	//读数据  --高8位
				
				OV7725_RCK=1; 
				color<<=8;  
				OV7725_RCK=0;
				color|=OV7725_DATA;	//读数  --低8位		(高低8+8位合并成一个u16发送)								
				OV7725_RCK=1;
				GPIOB->CRL=0X33333333;
			
				/*串口发送数据*/
				Send_Pic_Div(color);     //color:u16
				LCD_WR_DATA(color);      //显示一个像素点(RGB) 320*240中的一个				
		}
}

然后RGB565的一个像素是16位,两字节。在测试过程中发现,如果直接发送u16的color,那么图像将会偏绿。调试发现读取高字节R分量值为0。所以我还是选择将u16拆成低8位和高8位分开发送。

但是其实真正的原因在于上位机解析方法错了,发送方式并无影响,这点我们在后文说。

串口发送函数,先发高位,后发低位:

void Send_Pic_Div(u16 color)
{
		u8 temp;		 
		temp = color&0x00ff;						//低八位
		USART_SendData(USART1,temp);
		while(USART_GetFlagStatus(USART1,USART_FLAG_TC) != SET);
	
		temp = color>>8;								//高八位
		USART_SendData(USART1,temp);
		while(USART_GetFlagStatus(USART1,USART_FLAG_TC) != SET);
}

或者直接发u16的color像素:

void Send_Pic_All(u16 color)
{
		USART_SendData(USART1,color);
		while(USART_GetFlagStatus(USART1,USART_FLAG_TC) != SET);
}
	

最后需要注意,把串口其它发送数据部分注释掉,因为上位机把所有接受数据都认为是图像数据。我这里只注释了LCD初始化和TIM3中断里的两个printf()。

至此,STM32这边的程序就完成啦,接下来做上位机部分。

三、上位机接收,解析,显示保存

2.1、接收解析

上位机还是基于串口助手修改的,我直接用我之前写的串口助手修改了,基础功能不多(正是想要的)。需要重点修改的就是串口接收部分。

我们采用采用16进制接收数据。因为都是字节流,而且是不断发送过来的。所以接收到数据包就要解析。

这里需要强调一下,一个像素是16位,2字节,所以我们要拿到两个byte,也就是像素的高位和低位,然后再进行解析。我这样来取两字节:

 colorL = received_buf[i * 2];
 colorH = received_buf[i * 2 + 1];

还有Invoke最好套在最外面,因为里面会频繁的更新UI显示。
整个C#接收代码是这样的:

private void sp_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
   this.Invoke(new EventHandler(delegate
   {            
       //---------------------------//
       int num = sp.BytesToRead;      //获取接收缓冲区中的字节数
       byte[] received_buf = new byte[num];    //声明一个大小为num的字节数据用于存放读出的byte型数据
       receive_count += num;             //接收字节计数变量增加nun
       sp.Read(received_buf, 0, num);    //读取接收缓冲区中num个字节到byte数组中
       sb.Clear();                       //防止出错,首先清空字符串构造器
       if (isHex == true)
       {
           //u16 = 2bytes
           //这里是按byte读取,换成Int16也不行,需要一次读两个byte出来
           for (int i = 0; i < received_buf.Length; i++)
           {
               sb.Append(received_buf[i].ToString("X2") + ' ');    //将byte型数据转化为2位16进制文本显示,并用空格隔开
               if ((i+1) * 2 <= received_buf.Length)
               {
                   //读取一个像素
                   colorL = received_buf[i * 2];
                   colorH = received_buf[i * 2 + 1];
                   //解析RGB565
                   Int32 r, g, b;                        //0-255 , color 511
                   r = (colorH & 0xf8) >> 3;
                   g = ((colorH & 0x07) << 2) | ((colorL & 0xe0) >> 6);
                   b = colorL & 0x1f;
                   //Console.WriteLine("Red: "+r.ToString()+ " Green: " + g.ToString()+ " Blue: " + b.ToString());
                   //合成并显示像素,提高亮度
                   newColor = Color.FromArgb(r*5, g*5, b*5);
                   Int32 Row = (receive_count) / 320 / 2;    //计算列: 共240列,每列320个像素点
                   OvImage.SetPixel(Row, y++, newColor);
                   //换列显示
                   if (y == 320) { y = 0; }
               }                        
           }           
       }
       else
       {
           //选中ASCII模式显示
           sb.Append(Encoding.ASCII.GetString(received_buf));  //将整个数组解码为ASCII数组
       }            
       //更新UI显示
       ptbOv7725.Image = OvImage;        //放在外面按每次(一列)接收的来显示了          
       tbxRecvData.AppendText(sb.ToString());
       tbxRecvLength.Text =  receive_count.ToString() + "Bytes";
       //--------------------------------------//
   }));

}

2.2、数据格式转换

LCD上是RGB565,电脑上BMP是16位RGB555。所以我们需要对接收的数据进行格式转换。

先说一下这两种格式的数据。

  1. RGB565:

每个像素用16比特位表示,占2个字节,RGB分量分别使用5位、6位、5位。
在这里插入图片描述

//获取高字节的5个bit
R = color & 0xF800;
//获取中间6个bit
G = color & 0x07E0;
//获取低字节5个bit
B = color & 0x001F;

  1. RGB555

每个像素用16比特位表示,占2个字节,RGB分量都使用5位(最高位保留)。
在这里插入图片描述

//获取高字节的5个bit
R = color & 0x7C00;
//获取中间5个bit
G = color & 0x03E0;
//获取低字节5个bit
B = color & 0x001F;

所以我们最终的解析方法是这样的:

对于G分量由6转换成5,直接舍弃最低位(右移一位实现)。

提取R分量:将colorH右移3位,最后将剩余位清零。
提取G分量:将colorH左移2位,做G分量的高3位;将colorL右移6位,舍弃低位,其余做低2位,拼接在一起,最后将剩余位清零。
提取B分量:将colorL剩余位清零。
在这里插入图片描述

//解析RGB565
Int32 r, g, b;                        //0-255 , color 511
r = (colorH & 0xf8) >> 3;
g = ((colorH & 0x07) << 2) | ((colorL & 0xe0) >> 6);
b = colorL & 0x1f;

C#里面Bitmap类所指定的数据格式不知道为啥不好用,选择RGB555格式后就是黑底,很疑惑。

2.3、显示结果及存在问题

图像在第一列会有偏移:
在这里插入图片描述
接收数据少一字节:
在这里插入图片描述
上位机与LCD花屏部分相反(左上位机保存的图像,有LCD显示):
我猜测是丢失的一字节引起的。
在这里插入图片描述在这里插入图片描述
实验截图:
在这里插入图片描述

四、小结&开源

开发其实遇到了太多问题,数据解析,丢失字节,传输速度慢等等问题。在我的OneNote笔记上大概有10页多,

现在对于RGB565和RGB555数据格式有了了解,也知道该如何解析。OV7725模块的配置使用也比较了解。

该项目还是有一些问题未解决,但是我目前换了更简单的思路(后面会继续发出来),就不再继续深究了。如果有兴趣欢迎指导我一下。后面还要对上位机的启动方式做修改,让它能多次接收。最后能在解决那一字节的问题。传输时间有点长,可以提高波特率。

项目地址(STM32和上位机代码压缩在一处了):
CSDN:上位机+STM32
Github:上位机STM32

参考文章:
1、RGB888、RGB555、RGB565之间转换
2、stm32调用OV7670获取图像并通过蓝牙传输至PC

评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值