本篇文章由 VeriMake 旧版论坛中备份出的原帖的 Markdown 源码生成
原帖标题为:能追着颜色跑的小车|基于安路 FPGA 的 Verilog 实现
原帖网址为:https://verimake.com/topics/191 (旧版论坛网址,已失效)
原帖作者为:RIGHT(旧版论坛 id = 88,注册于 2021-04-20 17:02:45)
原帖由作者初次发表于 2021-04-20 17:42:52,最后编辑于 2021-04-20 17:42:52(编辑时间可能不准确)
截至 2021-12-18 14:27:30 备份数据库时,原帖已获得 1583 次浏览、0 个点赞、0 条回复


这次介绍一个比较简易的小车颜色追逐系统,其中使用安路的FPGA完成摄像头驱动,图片数据存储、颜色识别定位和图像显示,同时与Arduino进行通信以此驱动小车追逐目标物体。
系统架构
整个系统架构如下:

我们使用的是ov2640摄像头,通过摄像头驱动模块将采集到的图像数据缓存在RAM中,然后将数据从RAM中读出在VGA时序上进行颜色识别与定位,并将颜色识别与定位效果在VGA上显示。在VGA时序上进行处理是为了达到实时处理的效果。对于颜色识别与定位,主要是在YCbCr色域上进行阈值化,将指定的颜色显示出来,然后进行水平垂直计数得到指定颜色物体的中心坐标以及大小,最后通过IIC总线将所需的数据传给Arduino,驱使小车追逐颜色。这个颜色识别和定位也可以运用到其他应用场合中。如果大家想用其他摄像头也是可以的,相关摄像头驱动在网上有很多详实的资料。
FPGA代码讲解
RGB to YCbCr
YCbCr色域相较于RGB色域来说受亮度的影响较小,可以较好的完成后续的阈值化操作。RGB转YCbCr公式如下:

这里使用三级流水线完成该算法,第一级流水线实现所有的乘法运算。
always @(posedge pixelclk)begin
mult_r_for_y_18b <= i_r_8b * para_0183_10b;
mult_r_for_cb_18b <= i_r_8b * para_0101_10b;
mult_r_for_cr_18b <= i_r_8b * para_0439_10b;
end
always @(posedge pixelclk)begin
mult_g_for_y_18b <= i_g_8b * para_0614_10b;
mult_g_for_cb_18b <= i_g_8b * para_0338_10b;
mult_g_for_cr_18b <= i_g_8b * para_0399_10b;
end
always @(posedge pixelclk)begin
mult_b_for_y_18b <= i_b_8b * para_0062_10b;
mult_b_for_cb_18b <= i_b_8b * para_0439_10b;
mult_b_for_cr_18b <= i_b_8b * para_0040_10b;
end
第二级流水线实现所有括号中的运算。
always @(posedge pixelclk)begin
add_y_0_18b <= mult_r_for_y_18b + mult_g_for_y_18b;
add_y_1_18b <= mult_b_for_y_18b + para_16_18b;
add_cb_0_18b <= mult_b_for_cb_18b + para_128_18b;
add_cb_1_18b <= mult_r_for_cb_18b + mult_g_for_cb_18b;
add_cr_0_18b <= mult_r_for_cr_18b + para_128_18b;
add_cr_1_18b <= mult_g_for_cr_18b + mult_b_for_cr_18b;
end
第三级流水线实现所有运算的连接。
always @(posedge pixelclk)begin
y_tmp <= add_y_0_18b + add_y_1_18b;
cb_tmp <= add_cb_0_18b - add_cb_1_18b ;
cr_tmp <= add_cr_0_18b - add_cr_1_18b ;
end
二值化
这里已经给出了红色和蓝色的Y、Cb、Cr的高低阈值,当然也可以给入其他颜色的阈值,阈值的求得有很多方法,我们是通过在线调试获得这些阈值。
threshold_binary#(
.DW (24 ),
.Y_TH (150 ),//红色
.Y_TL (40 ),
.CB_TH (155 ),
.CB_TL (100 ),
.CR_TH (240 ),
.CR_TL (160 )
/* .Y_TH (135 ),//蓝色
.Y_TL (50 ),
.CB_TH (245 ),
.CB_TL (155 ),
.CR_TH (140 ),
.CR_TL (80 )
*/
)u_threshold_binary(
.pixelclk (clk_lcd ),
.reset_n (rst_n ),
.i_ycbcr (o_ycbcr ),
.i_hsync (o_hsync ),
.i_vsync (o_vsync ),
.i_de (o_de0 ),
.o_binary (th_binary ),
.o_hsync (th_hsyncr ),
.o_vsync (th_vsyncr ),
.o_de (th_de )
);
对于同时满足这些的阈值的像素显示黑色,否则显示白色,即实现二值化。以此就可以将指定颜色的物体显示出来。
always @(posedge pixelclk or negedge reset_n) begin
if(!reset_n)begin
binary_r <= 24'd0;
end
else begin
if(en0==1'b1 && en1 ==1'b1 && en2==1'b1) begin
binary_r <= 24'd0;
end
else begin
binary_r <= 24'hffffff;
end
end
end
水平垂直计数
最后需要通过水平和垂直计数框出物体以及得到中心坐标和像素大小,结合这张图能够较好的理解这个过程。

黑色像素点是阈值化后有效的像素点,白色像素点为无效像素点。首先一行中连续出现的黑色像素点个数要大于等于cnt_x0个时才能代表这行是有效行,以及连续出现这样的有效行要大于等于cnt_y0个的时候才能代表整个物体是目标物体。同时当该行被断定为有效行后我们还要判断后续出现的连续的无效像素点的个数是否大于等于cnt_x1个,是的话表示该行的有效像素点结束,否则继续对该行计数进行判断。最后在确定有目标物体的情况下还要判断后续的连续无效行的个数是否大于等于cnt_y1个,是的话表示目标物体搜索结束,否则继续进行计数判断。以此来框出目标同时也可以确定出目标物体的x0、x1、y0、y1以及目标物体大小。
为了让小车追逐目标,我们给Arduino传送x_error和h_error这两个数据,x_error为物体中心点x轴与屏幕中心点x轴的偏移量,h_error为目标物体的大小与我们所设定的大小的差值,这两个值可以根据需求设定。
always@(posedge clk_lcd or negedge rst_n)begin
if(!rst_n)begin
x_error<=32'd0;
end
else begin
x_error<=(mid_x-7'd100);
end
end
always@(posedge clk_lcd or negedge rst_n)begin
if(!rst_n)begin
h_error<=32'd0;
end
else begin
h_error<=(p_sum-2000);
end
end


到这里FPGA上的实现基本上是完成了,后面只需要通过IIC协议将所需的信息发给Arduino。
本项目中Arduino为主机,FPGA为从机,从机地址设置为ox12。
Arduino代码讲解
Arduino上的关键代码如下:
两个帧头:AA和AE,当连续接收到这两个数据后,后面开始传送有效数据。buff中存储从FPGA中传来的所需的数据,FPGA上是先发送x_error,再发送h_error.所以这里我们也是先接收 x_error,在接收h_error。
switch (state)
{
case 0:
if(data==(char)0xAA){ //帧头1
state=1;
}
else {
state=0;
}
break;
case 1:
if(data==(char)0xAE) state=2; //帧头2
else state=0;
break;
case 2:
buff[0]=data;
state=3;
break;
case 3:
buff[1]=data;
state=4;
break;
case 4:
buff[2]=data;
state=5;
break;
case 5:
buff[3]=data;
state=6;
break;
case 6:
buff[4]=data;
state=7;
break;
case 7:
buff[5]=data;
state=8;
break;
case 8:
buff[6]=data;
state=9;
break;
case 9:
buff[7]=data;
state=0;
break;
default:
state=0;
break;
}
x_error=((0x000000FF&(long)buff[0])|(((long)buff[1])<<8)|(((long)buff[2])<<16)|(((long)buff[3])<<24));
h_error=((0x000000FF&(long)buff[4])|(((long)buff[5])<<8)|(((long)buff[6])<<16)|(((long)buff[7])<<24));
这里设置从机地址和请求的字节个数,一定要保证这个地址和FPGA的地址一致
Wire.requestFrom(0x12, 10); //从机地址,字节个数
而x_error和h_error这两个参数是如何控制小车运动的呢?
当没有目标物体出现的时候,小车原地打转搜索目标;当目标物体大小与设定的大小的差值在某一个范围内,小车则前进;大于某一个范围,小车则后退。而小车的左右转动是通过x_error的二次函数来控制,这几个范围以及x_error的二次函数可以根据自己的需求去改变。
相关代码地址:https://github.com/verimake-team/verilogic.git
视频链接:https://b23.tv/f8ln1h