30、项目实战3:智能小车控制系统(电机驱动+摄像头循迹+超声波避障+蓝牙遥控)
终于到了咱们课程的收官项目了。说实话,每次带学员做这个智能小车项目,我都挺兴奋的。为什么?因为它把前面学的所有东西——PWM、I2C、UART、图像处理、状态机——全都串起来了。你想想看,一个巴掌大的FPGA板子,要同时管四个轮子、一个摄像头、一个超声波模块,还要跟手机蓝牙通信,这本身就是个系统工程。
我个人习惯把这种多模块项目叫做“FPGA的期末考试”。做完了它,你基本就能应付大部分嵌入式控制场景了。
30.1 系统架构与模块划分
先看整体架构。我一般会画一张模块图,但这里我们用文字描述:
- 顶层模块:top_smart_car,负责所有子模块的例化和互联
- 电机驱动模块:motor_driver,输出PWM波控制四个轮子
- 摄像头采集模块:camera_capture,通过DVP接口读取OV7670数据
- 循迹处理模块:line_tracking,分析图像中的黑线位置
- 超声波测距模块:ultrasonic,测量前方障碍物距离
- 蓝牙通信模块:bluetooth_uart,解析手机APP发来的指令
- 决策控制模块:control_fsm,根据各传感器数据决定小车行为
嗯,这里要注意:决策控制模块是整个系统的核心。它就像一个大脑,接收所有传感器的信息,然后决定是直行、转弯还是刹车。
模块间通信要点:
- 摄像头输出的是像素数据流,循迹模块需要缓存一行数据才能判断
- 超声波模块输出的是脉冲宽度,需要换算成厘米
- 蓝牙模块输出的是ASCII字符,需要解析成控制指令
- 所有模块的时钟域要统一处理,我建议都用系统主时钟
30.2 电机驱动模块设计
电机驱动说白了就是产生PWM波。但四个轮子怎么协调?我做过一个项目,小车一启动就往右偏,查了半天发现是左右电机的PWM占空比差了0.5%。
这里给出一个双路PWM生成器的核心代码:
module motor_driver (
input wire clk,
input wire rst_n,
input wire [7:0] speed_left, // 左轮速度 0-255
input wire [7:0] speed_right, // 右轮速度 0-255
input wire [1:0] dir_left, // 左轮方向 00:停止 01:前进 10:后退
input wire [1:0] dir_right, // 右轮方向
output reg pwm_left,
output reg pwm_right,
output reg [1:0] motor_left_io, // 左电机H桥控制
output reg [1:0] motor_right_io // 右电机H桥控制
);
reg [7:0] pwm_cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
pwm_cnt <= 8'd0;
else
pwm_cnt <= pwm_cnt + 1'b1;
end
// 左轮PWM
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
pwm_left <= 1'b0;
else
pwm_left <= (pwm_cnt < speed_left) ? 1'b1 : 1'b0;
end
// 右轮PWM
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
pwm_right <= 1'b0;
else
pwm_right <= (pwm_cnt < speed_right) ? 1'b1 : 1'b0;
end
// 方向控制逻辑
always @(*) begin
case (dir_left)
2'b00: motor_left_io = 2'b00; // 停止
2'b01: motor_left_io = 2'b10; // 前进
2'b10: motor_left_io = 2'b01; // 后退
default: motor_left_io = 2'b00;
endcase
// 右轮同理
end
endmodule
我的经验:实际调试时,左右轮的速度值很少完全相等。建议在顶层加一个校准参数,比如speed_left = base_speed,speed_right = base_speed + 5。这样小车才能走直线。
30.3 摄像头循迹模块
摄像头循迹,说白了就是让小车“看”到地上的黑线,然后跟着走。OV7670摄像头输出的是RGB565格式,我们需要把它转成灰度,再二值化。
我记得第一次做这个模块时,图像噪声特别大,黑线根本识别不出来。后来发现是摄像头时钟没配好,数据采样点不对。
循迹的核心算法其实很简单:
- 采集一行像素,比如320个点
- 对每个点做灰度转换:Gray = (R*77 + G*150 + B*29) >> 8
- 设定阈值,大于阈值为白色(地面),小于阈值为黑色(线)
- 统计这一行中黑色像素的分布,找到中心位置
- 根据中心位置偏离图像中线的程度,计算转向角度
module line_tracking (
input wire clk,
input wire vsync, // 帧同步
input wire href, // 行同步
input wire [7:0] pixel_data, // 灰度像素值
output reg [8:0] line_pos, // 黑线中心位置 0-319
output reg line_found // 是否检测到黑线
);
reg [8:0] pixel_cnt;
reg [8:0] black_sum;
reg [15:0] black_pos_sum;
always @(posedge clk) begin
if (vsync) begin
// 新一帧开始,清空统计
pixel_cnt <= 0;
black_sum <= 0;
black_pos_sum <= 0;
end
else if (href) begin
// 行有效期间,每个时钟一个像素
if (pixel_data < 8'd80) begin // 阈值80,可调
black_sum <= black_sum + 1;
black_pos_sum <= black_pos_sum + pixel_cnt;
end
pixel_cnt <= pixel_cnt + 1;
end
end
// 行结束时计算中心位置
always @(posedge clk) begin
if (!href && pixel_cnt > 0) begin
if (black_sum > 10) begin // 至少10个黑点才算有效
line_found <= 1'b1;
line_pos <= black_pos_sum / black_sum;
end
else begin
line_found <= 1'b0;
end
end
end
endmodule
注意:这个模块只处理了一行数据。实际项目中,我建议取中间三行做平均,或者取多行做加权平均,这样抗干扰能力更强。我曾经遇到过地面有污渍被误识别成黑线的情况,后来加了多行滤波就解决了。
30.4 超声波避障模块
超声波模块HC-SR04,原理很简单:发一个10us的高电平触发信号,然后等回波。回波脉冲的宽度就是距离。
公式:距离(cm) = 脉冲宽度(us) / 58
为什么是58?因为声速340m/s,来回距离除以2,再换算成厘米,就是1us对应0.017cm,取倒数约58。
module ultrasonic (
input wire clk,
input wire trig_out, // 触发信号,由状态机控制
input wire echo_in, // 回波信号
output reg [15:0] distance_cm // 距离,单位cm
);
reg [31:0] echo_width; // 回波宽度计数器
// 测量回波脉冲宽度
always @(posedge clk) begin
if (echo_in)
echo_width <= echo_width + 1;
else
echo_width <= 0;
end
// 换算成厘米,假设时钟频率50MHz
always @(posedge clk) begin
// 50MHz时钟,每个周期20ns
// 距离 = 脉冲宽度(20ns单位) * 20 / 1000 / 58
// 简化:距离 = 脉冲宽度 / 2900
distance_cm <= echo_width / 2900;
end
endmodule
避坑指南:我曾经遇到过超声波模块在强光下测距不准的问题。后来发现是模块的接收探头被阳光干扰了。解决办法是在探头上加一个遮光罩,或者用软件做多次测量取中值滤波。
30.5 蓝牙遥控与决策控制
蓝牙模块用的是HC-05,通过UART与FPGA通信。手机APP发送的指令格式我一般这样定义:
| 指令 | 含义 | 动作 |
|---|---|---|
| 'F' | 前进 | 四个轮子正转 |
| 'B' | 后退 | 四个轮子反转 |
| 'L' | 左转 | 左轮停止,右轮正转 |
| 'R' | 右转 | 右轮停止,左轮正转 |
| 'S' | 停止 | 所有轮子停止 |
| 'A' | 自动模式 | 启动循迹+避障 |
决策控制模块是一个状态机,它根据当前工作模式(手动/自动)和传感器数据,输出电机控制信号。
module control_fsm (
input wire clk,
input wire rst_n,
input wire [7:0] uart_cmd, // 蓝牙指令
input wire [8:0] line_pos, // 黑线位置
input wire line_found, // 是否找到线
input wire [15:0] distance, // 超声波距离
output reg [7:0] speed_left,
output reg [7:0] speed_right,
output reg [1:0] dir_left,
output reg [1:0] dir_right
);
reg auto_mode;
// 解析蓝牙指令
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
auto_mode <= 0;
speed_left <= 0;
speed_right <= 0;
dir_left <= 2'b00;
dir_right <= 2'b00;
end
else begin
case (uart_cmd)
"A": auto_mode <= 1; // 进入自动模式
"S": begin
auto_mode <= 0;
// 手动停止
speed_left <= 0;
speed_right <= 0;
end
// 其他手动指令...
endcase
if (auto_mode) begin
// 自动模式:循迹+避障
if (distance < 20) begin
// 前方有障碍,优先避障
// 后退并右转
dir_left <= 2'b10;
dir_right <= 2'b10;
speed_left <= 8'd100;
speed_right <= 8'd100;
end
else if (line_found) begin
// 循迹模式
if (line_pos > 180) begin
// 偏右,左转
dir_left <= 2'b01;
dir_right <= 2'b00;
speed_left <= 8'd80;
speed_right <= 8'd0;
end
else if (line_pos < 140) begin
// 偏左,右转
dir_left <= 2'b00;
dir_right <= 2'b01;
speed_left <= 8'd0;
speed_right <= 8'd80;
end
else begin
// 居中,直行
dir_left <= 2'b01;
dir_right <= 2'b01;
speed_left <= 8'd120;
speed_right <= 8'd120;
end
end
else begin
// 没找到线,原地旋转搜索
dir_left <= 2'b01;
dir_right <= 2'b10;
speed_left <= 8'd60;
speed_right <= 8'd60;
end
end
end
end
endmodule
30.6 项目调试心得
这个项目调试起来,说难不难,说简单也不简单。我总结几个关键点:
- 先分后总:每个模块单独仿真验证,再联调。我见过有人一上来就全连上,结果出了问题根本不知道是哪个模块的锅。
- 摄像头要调焦:OV7670的镜头是可以旋转的,离地高度不同,焦距也不同。我一般先拍一张图,看看清不清楚。
- 超声波有盲区:HC-SR04的最小测量距离约2cm,小于这个距离会输出错误值。代码里要做保护。
- 蓝牙波特率要匹配:HC-05默认是9600,但有些模块是38400。记得先确认,不然收不到数据。
最终效果:当所有模块协同工作时,小车可以做到:
- 在白色地面上沿着黑色胶带自动行驶
- 遇到障碍物自动停车并绕行
- 通过手机APP随时切换手动/自动模式
- 手动模式下响应延迟小于50ms
嗯,这个项目做完,你基本就掌握了FPGA在嵌入式控制领域的全部技能。以后不管是做机器人、无人机还是工业控制,核心思路都是一样的——传感器采集、数据处理、决策输出。说白了,FPGA就是那个“快”字,所有事情并行处理,毫秒级响应。
好了,这一章的内容就到这里。下一章我们会做一个更复杂的项目——基于FPGA的实时图像边缘检测系统,到时候会用到Sobel算子,敬请期待。