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格式,我们需要把它转成灰度,再二值化。

我记得第一次做这个模块时,图像噪声特别大,黑线根本识别不出来。后来发现是摄像头时钟没配好,数据采样点不对。

循迹的核心算法其实很简单:

  1. 采集一行像素,比如320个点
  2. 对每个点做灰度转换:Gray = (R*77 + G*150 + B*29) >> 8
  3. 设定阈值,大于阈值为白色(地面),小于阈值为黑色(线)
  4. 统计这一行中黑色像素的分布,找到中心位置
  5. 根据中心位置偏离图像中线的程度,计算转向角度
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算子,敬请期待。