FPGA实习项目实战:2026年用Zynq实现智能小车控制

二牛学FPGA
文章2026-05-04
62

Quick Start

  • 步骤一:准备硬件——Zynq-7010/7020开发板(如PYNQ-Z2、ZedBoard)、电机驱动模块(L298N)、直流电机×2、超声波测距模块(HC-SR04)、红外循迹模块×2、电池组(7.4V~12V)。
  • 步骤二:安装Vivado 2024.2(或更高版本)及Vitis统一平台,确保支持Zynq-7000系列。
  • 步骤三:在Vivado中创建新工程,选择xc7z020clg484-1(PYNQ-Z2)或对应器件。
  • 步骤四:添加Zynq Processing System IP核,配置DDR(512MB)、UART1(115200 baud)、GPIO MIO(复位按键)和AXI GPIO(用于电机PWM与传感器输入)。
  • 步骤五:编写顶层Verilog模块,例化Zynq PS与自定义RTL模块(PWM发生器、超声波测距控制器、循迹逻辑)。
  • 步骤六:综合、实现并生成比特流,导出硬件描述(XSA文件)至Vitis。
  • 步骤七:在Vitis中创建裸机应用工程,编写C代码控制小车:初始化GPIO、PWM占空比调整、传感器读取、避障与循迹状态机。
  • 步骤八:下载比特流与软件ELF到开发板,连接电机与传感器,上电后小车应能自动避障或循迹(根据模式选择)。
  • 验收点:小车在平坦地面直线行驶不跑偏,遇障碍物20cm内停止或转向,循迹时能沿黑线行驶。

前置条件与环境

项目/推荐值说明替代方案
器件/板卡Zynq-7020(xc7z020clg484-1)Zynq-7010(xc7z010)或Artix-7+外部MCU(不推荐)
EDA版本Vivado 2024.2 + Vitis 2024.2Vivado 2023.x(需调整IP核版本)
仿真器Vivado Simulator(XSim)ModelSim/Questa(需编译库)
时钟/复位PS侧50MHz晶振,PL侧由PS提供100MHz时钟;复位使用PS复位输出外部晶振+复位芯片(增加BOM)
接口依赖PMOD接口连接传感器,GPIO连接电机驱动模块Arduino Shield兼容板(如Pmod Shield)
约束文件XDC约束:时钟周期10ns,I/O标准LVCMOS33,输入延迟2ns自动推导(不推荐,可能导致时序违规)
电源5V/2A直流电源(板卡供电)+ 7.4V电池组(电机供电)12V电池组+降压模块

目标与验收标准

  • 功能点:
    • 电机控制:两路PWM(频率1kHz,占空比0%~100%),实现前进、后退、左转、右转、停止。
    • 超声波测距:测量距离2cm~200cm,精度±1cm,更新频率≥20Hz。
    • 红外循迹:检测黑线/白地,输出数字信号(0/1),响应时间<10ms。
    • 避障模式:前方20cm恢复直行。
    • 循迹模式:根据左右传感器差值调整转向,保持沿黑线行驶。
  • 性能指标:
    • Fmax:PL逻辑时钟≥100MHz(典型值150MHz)。
    • 资源占用:LUT<2000,FF<1500,BRAM<4个,DSP<2个。
    • 延迟:从传感器输入到电机输出≤1ms(不含机械惯性)。
  • 验收方式:
    • 上板测试:小车在1m×2m测试场地完成避障与循迹各3次,成功率100%。
    • 仿真验证:PWM波形占空比误差<1%,超声波测距时序符合数据手册。
    • 日志输出:UART打印传感器值与状态机状态,无异常。

实施步骤

工程结构

car_controller/
├── vivado/
│   ├── car_controller.xpr          # Vivado工程文件
│   ├── src/
│   │   ├── top.v                   # 顶层模块(例化PS与RTL)
│   │   ├── pwm_gen.v               # PWM发生器
│   │   ├── ultrasonic_ctrl.v       # 超声波测距控制器
│   │   ├── line_tracker.v          # 循迹逻辑
│   │   └── car_fsm.v               # 避障/循迹状态机
│   ├── constr/
│   │   └── car_controller.xdc      # 时序与I/O约束
│   └── ip/                         # IP核目录(Zynq PS, AXI GPIO)
├── vitis/
│   ├── car_app/
│   │   ├── src/
│   │   │   ├── main.c             # 主控逻辑
│   │   │   ├── gpio_drv.c         # GPIO驱动
│   │   │   ├── pwm_drv.c          # PWM驱动
│   │   │   └── sensor_drv.c       # 传感器驱动
│   │   └── lscript.ld             # 链接脚本
│   └── platform/                   # 硬件平台描述
└── README.md

逐行说明

  • 第1行:工程根目录,vivado/存放硬件设计,vitis/存放软件。
  • 第2行:Vivado工程文件,双击打开。
  • 第3~7行:RTL源文件,每个模块独立文件,便于复用与调试。
  • 第8行:约束文件,定义时钟、I/O标准与输入延迟。
  • 第9行:IP核目录,由Vivado自动管理。
  • 第10~15行:Vitis应用工程,main.c为主循环,驱动文件封装底层操作。
  • 第16行:链接脚本,定义内存布局(通常无需修改)。
  • 第17行:硬件平台描述,由Vivado导出。
  • 第18行:项目说明文档。

关键模块:PWM发生器

// pwm_gen.v
module pwm_gen #(
    parameter CLK_FREQ = 100_000_000, // 输入时钟频率(Hz)
    parameter PWM_FREQ = 1_000,       // PWM频率(Hz)
    parameter RESOLUTION = 8          // 占空比分辨率(位)
) (
    input  wire clk,
    input  wire rst_n,
    input  wire [RESOLUTION-1:0] duty, // 占空比(0~2^RESOLUTION-1)
    output reg pwm_out
);
    localparam COUNTER_MAX = CLK_FREQ / PWM_FREQ; // 计数器最大值
    localparam SCALE = COUNTER_MAX / (2**RESOLUTION); // 每单位占空比对应计数值
    reg [31:0] counter;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            counter <= 0;
            pwm_out = COUNTER_MAX - 1) begin
                counter <= 0;
            end else begin
                counter <= counter + 1;
            end
            pwm_out <= (counter < duty * SCALE) ? 1 : 0;
        end
    end
endmodule

逐行说明

  • 第1~2行:模块定义,参数化时钟频率、PWM频率和分辨率,便于移植。
  • 第3~8行:端口声明,clk和rst_n为全局信号,duty为输入占空比,pwm_out为输出。
  • 第9行:计算计数器最大值,例如100MHz/1kHz=100000。
  • 第10行:计算每单位占空比对应的计数值,例如100000/256≈390。
  • 第11行:32位计数器,足够覆盖最大值。
  • 第12~14行:异步复位,计数器与输出清零。
  • 第15~18行:计数器从0到COUNTER_MAX-1循环。
  • 第19行:当计数器小于duty*SCALE时输出高电平,否则低电平,实现占空比控制。
  • 综合意图:生成一个周期为1ms的PWM波形,占空比由duty控制,用于驱动电机。
  • 仿真影响:需验证duty=0时pwm_out始终为0,duty=255时接近100%占空比。

关键模块:超声波测距控制器

// ultrasonic_ctrl.v
module ultrasonic_ctrl #(
    parameter CLK_FREQ = 100_000_000
) (
    input  wire clk,
    input  wire rst_n,
    output reg trig,         // 触发信号(10us高脉冲)
    input  wire echo,        // 回波信号(高电平宽度对应距离)
    output reg [15:0] distance_cm // 距离(单位:cm)
);
    localparam TRIG_PULSE = CLK_FREQ / 100_000; // 10us
    localparam SOUND_SPEED = 343; // 声速m/s
    localparam CM_PER_COUNT = (SOUND_SPEED * 100) / (CLK_FREQ / 2); // 每个时钟周期对应距离(cm)
    reg [31:0] counter;
    reg [3:0] state;
    reg [31:0] echo_width;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            trig <= 0;
            distance_cm <= 0;
            state <= 0;
            counter <= 0;
            echo_width <= 0;
        end else begin
            case (state)
                0: begin // 等待触发
                    trig <= 1;
                    if (counter < TRIG_PULSE - 1) begin
                        counter <= counter + 1;
                    end else begin
                        trig <= 0;
                        counter <= 0;
                        state <= 1;
                    end
                end
                1: begin // 等待echo上升沿
                    if (echo) begin
                        counter <= 0;
                        state <= 2;
                    end
                end
                2: begin // 测量echo高电平宽度
                    if (echo) begin
                        counter <= counter + 1;
                    end else begin
                        echo_width <= counter;
                        distance_cm <= counter * CM_PER_COUNT;
                        counter <= 0;
                        state <= 3;
                    end
                end
                3: begin // 等待50ms再触发下一次
                    if (counter < CLK_FREQ / 20) begin
                        counter <= counter + 1;
                    end else begin
                        counter <= 0;
                        state <= 0;
                    end
                end
                default: state <= 0;
            endcase
        end
    end
endmodule

逐行说明

  • 第1~2行:模块定义,参数化时钟频率。
  • 第3~8行:端口,trig输出10us脉冲,echo输入回波,distance_cm输出距离。
  • 第9行:计算10us所需的时钟周期数,100MHz下为1000。
  • 第10行:声速343m/s。
  • 第11行:每个时钟周期对应距离,公式为(343*100)/(100MHz/2)≈0.000686cm,实际使用整数运算。
  • 第12~14行:内部寄存器,counter为通用计数器,state为状态机,echo_width暂存回波宽度。
  • 第15~21行:异步复位,所有寄存器清零。
  • 第22~33行:状态0,输出10us触发脉冲,然后进入状态1。
  • 第34~38行:状态1,等待echo上升沿,检测到后重置计数器并进入状态2。
  • 第39~47行:状态2,测量echo高电平宽度,下降沿到来时计算距离并进入状态3。
  • 第48~54行:状态3,等待50ms(20Hz更新率)后再触发下一次测量。
  • 综合意图:实现HC-SR04的时序控制,直接输出厘米级距离。
  • 仿真影响:需验证trig脉冲宽度为10us,echo宽度与distance_cm成正比。

时序与约束

# car_controller.xdc
create_clock -period 10.000 -name sys_clk [get_ports {clk}]
set_input_delay -clock sys_clk -max 2.000 [get_ports {echo}]
set_input_delay -clock sys_clk -min 0.500 [get_ports {echo}]
set_output_delay -clock sys_clk -max 4.000 [get_ports {pwm_out}]
set_output_delay -clock sys_clk -min 1.000 [get_ports {pwm_out}]
set_property IOSTANDARD LVCMOS33 [get_ports {trig}]
set_property IOSTANDARD LVCMOS33 [get_ports {echo}]
set_property IOSTANDARD LVCMOS33 [get_ports {pwm_out}]
set_property PACKAGE_PIN L15 [get_ports {clk}]  ;# 示例引脚,需根据原理图修改

逐行说明

  • 第1行:创建100MHz时钟,周期10ns。
  • 第2~3行:输入延迟约束,echo信号最大延迟2ns,最小0.5ns,确保建立/保持时间。
  • 第4~5行:输出延迟约束,pwm_out最大延迟4ns,最小1ns。
  • 第6~8行:设置I/O标准为LVCMOS33(3.3V)。
  • 第9行:指定时钟引脚位置,需根据开发板原理图修改。
  • 常见坑:未约束输入延迟可能导致时序分析不准确,上板出现偶发错误。

验证

// tb_pwm_gen.v 测试PWM发生器
module tb_pwm_gen;
    reg clk, rst_n;
    reg [7:0] duty;
    wire pwm_out;
    pwm_gen #(.CLK_FREQ(100_000_000), .PWM_FREQ(1_000), .RESOLUTION(8)) uut (
        .clk(clk), .rst_n(rst_n), .duty(duty), .pwm_out(pwm_out)
    );
    initial begin
        clk = 0;
        forever #5 clk = ~clk; // 100MHz
    end
    initial begin
        rst_n = 0; #100 rst_n = 1;
        duty = 128; // 50%占空比
        #1000;
        duty = 64;  // 25%占空比
        #1000;
        $finish;
    end
endmodule

逐行说明

  • 第1~2行:测试模块,无端口。
  • 第3~5行:声明信号并例化UUT。
  • 第6~8行:生成100MHz时钟,周期10ns。
  • 第9~13行:复位后设置duty=128(50%),运行1ms后改为64(25%),再运行1ms结束。
  • 预期结果:pwm_out占空比在第一个1ms内为50%,第二个1ms内为25%。
  • 失败先查什么:检查duty*SCALE是否溢出,counter是否从0开始。

上板

  • 步骤:连接开发板JTAG → 下载比特流 → 运行Vitis应用 → 观察小车行为。
  • 常见坑:电机驱动模块需要独立电源,不能从开发板取电(电流不足)。
  • 排查:如果小车不动,检查PWM输出引脚电平是否正常(万用表测电压)。

原理与设计说明

为什么用Zynq而不是纯MCU?Zynq的PL部分可以并行处理传感器与电机控制,延迟低至微秒级,而PS部分运行状态机与通信,分工明确。关键trade-off:

  • 资源 vs Fmax:PWM发生器使用计数器而非DDS,资源少但频率固定;若需变频PWM,可改用DDS但消耗更多DSP。
  • 吞吐 vs 延迟:超声波测距采用状态机而非中断,吞吐固定20Hz,但延迟可控(无中断抖动)。
  • 易用性 vs 可移植性:AXI GPIO接口简单但占用PL资源;若用EMIO可减少资源但时序复杂。

验证与结果

指标实测值(示例)测量条件
Fmax150 MHzVivado时序报告,最差路径在PWM计数器
LUT占用1,234综合后报告,含PS与PL逻辑
FF占用987综合后报告
BRAM占用2用于AXI GPIO缓冲
PWM占空比误差<0.5%示波器测量,duty=128时占空比49.8%
超声波测距误差±1.2 cm标准障碍物(10cm/50cm/100cm)各测10次
循迹响应时间8 ms从传感器变化到电机转向

以上数值为典型配置下的示例结果,实际以工程与数据手册为准。

故障排查(Troubleshooting)

  • 现象1:小车不启动,电源指示灯不亮。原因:电源反接或电压不足。检查点:万用表测电源输出。修复:更换电池或重新接线。
  • 现象2:电机嗡嗡响但不转。原因:PWM频率过低或占空比太小。检查点:示波器测PWM引脚。修复:提高PWM频率至1kHz以上,占空比>10%。
  • 现象3:超声波测距始终为0。原因:trig脉冲宽度不足或echo未连接。检查点:示波器测trig与echo。修复:确认trig脉冲为10us,echo引脚正确。
  • 现象4:循迹传感器无反应。原因:传感器阈值设置错误或环境光干扰。检查点:调节传感器电位器。修复:重新校准阈值。
  • 现象5:小车直线行驶跑偏。原因:两轮电机转速不一致。检查点:测量两路PWM占空比。修复:软件中微调左右电机占空比。
  • 现象6:Vivado综合报错“Unconstrained path”。原因:未添加时序约束。检查点:查看综合日志。修复:添加XDC约束。
  • 现象7:Vitis下载失败“No compatible device”。原因:JTAG驱动未安装或板卡未上电。检查点:设备管理器查看。修复:安装驱动或重新上电。
  • 现象8:小车避障时频繁转向。原因:超声波测距更新率太低或噪声。检查点:串口打印距离值。修复:增加滤波(中值平均)。
  • 现象9:循迹时冲出赛道。原因:转向增益过大。检查点:观察转向角度。修复:减小PID的P项。
  • 现象10:电池续航不足10分钟。原因:电机驱动效率低或电池容量小。检查点:测量电机电流。修复:更换大容量电池或优化PWM频率。

扩展与下一步

  • 扩展1:加入蓝牙/WiFi模块(如ESP8266),实现手机APP遥控。
  • 扩展2:使用PID算法优化循迹与避障,提高稳定性。
  • 扩展3:增加摄像头(OV7670)与图像处理,实现视觉循迹。
  • 扩展4:参数化PWM频率与分辨率,通过PS寄存器动态配置。
分类
技术分享
标签
fpgaZYNQ智能小车
浏览 62
分享:

相关推荐

同频道 · 相近分类

暂无相关推荐

作者

二牛学FPGA查看主页

同分类阅读

文章

延伸阅读与实操

  • 文章 + 课程联动深度文章常对应体系课章节,可一键选课。
  • 学习产出可参考笔记与作业案例在学习产出广场持续更新。

探索全站