图片上位机
一、思路
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。所以我们需要对接收的数据进行格式转换。
先说一下这两种格式的数据。
- RGB565:
每个像素用16比特位表示,占2个字节,RGB分量分别使用5位、6位、5位。
//获取高字节的5个bit
R = color & 0xF800;
//获取中间6个bit
G = color & 0x07E0;
//获取低字节5个bit
B = color & 0x001F;
- 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
本文转载自https://blog.csdn.net/qq_37832932/article/details/104975339