Quick Start
- 准备硬件:Zynq-7020 开发板(如 Zedboard、PYNQ-Z2)、电机驱动模块(L298N 或 TB6612)、直流电机 ×2、编码器 ×2、超声波测距模块(HC-SR04)、红外循迹模块 ×2、12V 锂电池组。
- 安装 EDA 工具:Vivado 2024.2(含 Vitis)、Xilinx SDK 2024.2。确保安装时勾选“Zynq-7000”器件库。
- 创建 Vivado 工程:器件选 xc7z020clg484-1,新建 Block Design,添加 Zynq Processing System IP 并配置 PS 侧(DDR、UART、GPIO MIO)。
- 添加 PL 外设 IP:在 Block Design 中添加 AXI GPIO(用于电机 PWM 与编码器捕获)、AXI Timer(用于超声测距回波计时)、AXI Quad SPI(用于红外循迹 ADC 读取,如使用模拟输出模块)。
- 生成比特流并导出硬件:综合→实现→生成比特流。File → Export → Export Hardware(含比特流)。
- 启动 Vitis 创建应用工程:基于导出的硬件平台,新建 C/C++ 应用工程,选择“Empty Application”。
- 编写控制程序:初始化 GPIO、Timer、中断;实现 PID 速度闭环控制(编码器反馈)、超声避障逻辑、循迹阈值判断。代码见“实施步骤”章节。
- 编译并下载:编译生成 .elf,通过 JTAG 下载到开发板。上电后小车应自动执行预设模式(如循迹前进,遇障碍后退转向)。
- 验收:串口终端(115200 baud)打印速度、距离、模式状态;小车在直线赛道上以 0.3 m/s 速度循迹,误差 < 5 cm;遇 30 cm 内障碍物自动停车并转向。
前置条件与环境
| 项目/推荐值 | 说明 | 替代方案 |
|---|---|---|
| 器件/板卡 | Xilinx Zynq-7020 (xc7z020clg484-1) | Zynq-7010 (资源少,慎用);PYNQ-Z2 兼容 |
| EDA 版本 | Vivado 2024.2 + Vitis 2024.2 | Vivado 2023.x(需注意 IP 版本兼容性) |
| 仿真器 | Vivado Simulator 或 ModelSim SE-64 2024 | QuestaSim(需自行编译库) |
| 时钟/复位 | PS 侧 50 MHz 晶振;PL 侧使用 PS 输出的 FCLK_CLK0 (100 MHz) | 外部 PL 时钟(需约束) |
| 接口依赖 | UART (MIO 14/15) 用于调试;AXI GPIO 连接电机 PWM 与编码器 | 自定义 IOB 约束(非 AXI) |
| 约束文件 | XDC 需包含:时钟周期 10 ns(100 MHz)、所有 PL 引脚位置/电平(3.3V LVCMOS) | 自动推导(不推荐) |
| 电源 | 12V 2A 锂电池组供电机;板卡 5V/2A 适配器 | 单电源方案需注意纹波 |
目标与验收标准
- 功能点:
- 性能指标:
- 验收方式:
实施步骤
阶段一:工程结构与硬件设计
- 创建 Vivado 工程:选择 RTL Project,勾选“Do not specify sources at this time”。器件选择 xc7z020clg484-1。
- 搭建 Block Design:
- 连接与地址映射:运行自动连接,确保所有 AXI 外设挂载到 PS 的 M_AXI_GP0 口。地址范围默认(0x4000_0000–0x4001_FFFF 等)。
- 生成顶层 HDL:右键 Block Design → Create HDL Wrapper(选择“Let Vivado manage wrapper”)。
- 添加约束文件:新建 .xdc,写入以下关键约束:
# 时钟约束(100 MHz)
create_clock -period 10.000 -name clk_100 [get_ports {FCLK_CLK0}]
# 电机 PWM 引脚(PL 侧,3.3V)
set_property PACKAGE_PIN Y9 [get_ports {pwm_left}]
set_property IOSTANDARD LVCMOS33 [get_ports {pwm_left}]
set_property PACKAGE_PIN AA9 [get_ports {pwm_right}]
set_property IOSTANDARD LVCMOS33 [get_ports {pwm_right}]
# 编码器输入
set_property PACKAGE_PIN AB10 [get_ports {enc_left_a}]
set_property IOSTANDARD LVCMOS33 [get_ports {enc_left_a}]
set_property PACKAGE_PIN AB11 [get_ports {enc_left_b}]
set_property IOSTANDARD LVCMOS33 [get_ports {enc_left_b}]逐行说明
- 第 1 行:`create_clock` 约束定义 100 MHz 时钟周期为 10 ns,名称 `clk_100`,用于时序分析。
- 第 4 行:`set_property PACKAGE_PIN Y9` 将顶层端口 `pwm_left` 绑定到 FPGA 的 Y9 引脚(具体引脚号以板卡原理图为准)。
- 第 5 行:`IOSTANDARD LVCMOS33` 设置该引脚为 3.3V CMOS 电平,与电机驱动模块逻辑电平匹配。
- 第 6–7 行:同理定义右电机 PWM 引脚。
- 第 10–13 行:编码器 A/B 相输入引脚约束,同样 3.3V 电平。
阶段二:关键模块 RTL 设计(PL 侧)
本节实现电机 PWM 生成与编码器计数模块,均在 PL 侧用 Verilog 编写,通过 AXI GPIO 与 PS 交互。
// pwm_gen.v
module pwm_gen (
input wire clk, // 100 MHz
input wire rst_n, // 异步复位,低有效
input wire [7:0] duty_cycle, // 占空比 0-255
output reg pwm_out
);
reg [7:0] counter;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
counter <= 8'd0;
pwm_out <= 1'b0;
end else begin
counter <= counter + 1'b1;
if (counter < duty_cycle)
pwm_out <= 1'b1;
else
pwm_out <= 1'b0;
end
end
endmodule逐行说明
- 第 1–7 行:模块端口声明。`clk` 为 100 MHz 系统时钟,`rst_n` 低有效复位,`duty_cycle` 是 8 位无符号占空比输入(0–255 对应 0%–100%),`pwm_out` 为输出。
- 第 9 行:`reg [7:0] counter` 定义 8 位计数器,从 0 递增到 255 循环,周期 = 256 / 100 MHz = 2.56 µs,PWM 频率约 390 kHz。若需更低频率(如 1 kHz),可增加分频。
- 第 11–18 行:时序逻辑。复位时清零。每个时钟上升沿计数器加 1。当 `counter < duty_cycle` 时输出高电平,否则低电平。注意:`duty_cycle=0` 时始终低,`duty_cycle=255` 时 255/256 高,非 100%——若需 100% 可做边界处理。
// encoder_counter.v
module encoder_counter (
input wire clk,
input wire rst_n,
input wire enc_a,
input wire enc_b,
output reg [15:0] position
);
reg [1:0] a_prev, b_prev;
wire a_rise, a_fall;
// 边沿检测
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
a_prev <= 2'b0;
b_prev <= 2'b0;
end else begin
a_prev <= {a_prev[0], enc_a};
b_prev <= {b_prev[0], enc_b};
end
end
assign a_rise = (a_prev == 2'b01);
assign a_fall = (a_prev == 2'b10);
// 四倍频计数
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
position <= 16'd0;
else if (a_rise) begin
if (enc_b)
position <= position - 1'b1; // 反转
else
position <= position + 1'b1; // 正转
end else if (a_fall) begin
if (enc_b)
position <= position + 1'b1;
else
position <= position - 1'b1;
end
end
endmodule逐行说明
- 第 1–7 行:模块端口。`enc_a`、`enc_b` 为编码器两相信号,`position` 输出 16 位有符号位置计数(实际用无符号,软件可解释为有符号)。
- 第 9–10 行:`a_prev`、`b_prev` 为 2 位移位寄存器,用于边沿检测。
- 第 12–19 行:每个时钟沿将当前 `enc_a` 移入 `a_prev[0]`,`a_prev[1]` 保留上一拍值。同理 `b_prev`。
- 第 21–22 行:`a_rise` 在 `a_prev` 从 0→1 时(即 `a_prev == 2'b01`)为高;`a_fall` 在 1→0 时(`2'b10`)为高。
- 第 24–36 行:在 A 相上升沿或下降沿时,根据 B 相电平判断方向。若 `enc_b` 为高,则反转(减计数);为低则正转(加计数)。实现四倍频(每圈脉冲数 ×4),提高分辨率。
阶段三:PS 侧软件(C 语言控制逻辑)
在 Vitis 中创建应用工程,编写主控程序。核心功能:PID 速度控制、超声测距、循迹判断。
#include <stdio.h>
#include "xgpio.h" // AXI GPIO 驱动
#include "xtmrctr.h" // AXI Timer 驱动
#include "xparameters.h" // 外设基地址定义
#define PWM_PERIOD_MS 10 // 10 ms 控制周期
XGpio gpio_inst; // 电机 PWM 与编码器 GPIO
XTmrCtr timer_inst; // 超声测距定时器
int main() {
// 初始化外设
XGpio_Initialize(&gpio_inst, XPAR_AXI_GPIO_0_DEVICE_ID);
XTmrCtr_Initialize(&timer_inst, XPAR_AXI_TIMER_0_DEVICE_ID);
// 设置 GPIO 方向:通道1输出,通道2输入
XGpio_SetDataDirection(&gpio_inst, 1, 0x00); // 输出
XGpio_SetDataDirection(&gpio_inst, 2, 0xFF); // 输入
u16 position = 0;
u16 target_speed = 100; // 目标速度(编码器计数/周期)
s32 error, integral = 0, derivative;
s32 prev_error = 0;
float kp = 0.5, ki = 0.1, kd = 0.05;
while (1) {
// 读取编码器位置
position = XGpio_DiscreteRead(&gpio_inst, 2);
// 计算误差(假设 position 为速度测量值)
error = target_speed - position;
integral += error;
derivative = error - prev_error;
// PID 输出
s32 output = (s32)(kp * error + ki * integral + kd * derivative);
if (output > 255) output = 255;
if (output < 0) output = 0;
// 写 PWM 占空比
XGpio_DiscreteWrite(&gpio_inst, 1, (u32)output);
prev_error = error;
// 延时 10 ms(简单忙等,实际可用定时器中断)
for (volatile int i = 0; i < 1000000; i++);
}
return 0;
}逐行说明
- 第 1–5 行:包含头文件。`xgpio.h` 提供 AXI GPIO API,`xtmrctr.h` 提供定时器 API,`xparameters.h` 包含硬件平台生成的常量(如设备 ID)。
- 第 7 行:定义控制周期 10 ms,实际通过忙等近似实现,不精确。生产级应使用定时器中断。
- 第 9–10 行:声明 GPIO 和定时器实例。
- 第 14–15 行:`XGpio_Initialize` 通过设备 ID 初始化外设。ID 值在 `xparameters.h` 中定义(如 `XPAR_AXI_GPIO_0_DEVICE_ID`)。
- 第 18–19 行:`SetDataDirection` 设置通道方向。参数 2 为通道号,0x00 表示全部输出,0xFF 全部输入。
- 第 21–25 行:定义 PID 变量。`target_speed` 是期望的编码器计数(每 10 ms 的脉冲数),`kp`、`ki`、`kd` 为比例、积分、微分系数(浮点数,实际可定点化)。
- 第 27–44 行:主循环。读取位置 → 计算误差 → PID 运算 → 限幅 → 写 PWM → 更新前次误差 → 延时。注意:`integral` 未做抗饱和处理,长时间积分可能溢出,可增加限幅。
- 第 43 行:忙等延时,CPU 占用 100%。实际应使用 `sleep()` 或定时器中断。
常见坑与排查(阶段一至三)
- 坑 1:Block Design 中 AXI 外设地址冲突。现象:综合报错“Address overlap”。解决:在 Address Editor 中手动分配唯一地址范围,或让 Vivado 自动分配。
- 坑 2:编码器计数方向反。现象:电机正转时位置递减。解决:交换编码器 A/B 相接线,或在软件中取反。
- 坑 3:PWM 频率过高导致电机啸叫。现象:电机发出尖锐噪声。解决:增加分频器,使 PWM 频率降至 1–10 kHz。
- 坑 4:Vitis 工程编译报错“undefined reference”。原因:未链接驱动库。解决:在工程属性中勾选“xgpio”、“xtmrctr”等库。
原理与设计说明
为什么用 Zynq 而非纯 FPGA? 智能小车控制需要实时性高的 PID 算法(PS 侧运行 C 代码灵活)和高速编码器捕获(PL 侧硬件计数低延迟)。Zynq 的 PS+PL 异构架构正好满足:PS 做决策与通信,PL 做高速 I/O 与定时。
PID 控制周期选择 10 ms 的原因:电机机械时间常数通常在 10–100 ms 量级,10 ms 控制周期足以覆盖动态响应,同时避免过高的 CPU 负载。若周期过短(如 1 ms),PL 侧需额外硬件加速;过长(> 50 ms)会导致速度波动大。
编码器四倍频 vs 单倍频:四倍频利用 A/B 相所有边沿,每圈脉冲数 ×4,分辨率提高 4 倍,但 PL 侧逻辑略增(约 20 LUT)。对于低速小车(0.1–0.5 m/s),单倍频已够用,但四倍频可减少低速时的量化噪声。
超声测距为何用 AXI Timer 捕获模式? 超声波模块需要精确测量回波脉冲宽度(微秒级)。PS 侧软件轮询 GPIO 会因中断延迟产生误差,而 PL 侧定时器硬件捕获可达到 ±1 µs 精度,对应距离精度 ±0.17 mm。
验证与结果
| 验证项 | 测量条件 | 结果(示例值) | 说明 |
|---|---|---|---|
| PWM 频率 | 示波器探头接 PWM 引脚 | 390 kHz(无分频)/ 1 kHz(分频后) | 390 kHz 可驱动小电机,但可能啸叫标签:如需转载,请注明出处:https://z.shaonianxue.cn/41129.html ![]() ![]() ![]() ![]() Verilog中parameter与localparam的区别及模块参数化设计实践指南![]() FPGA时序与并行计算快速上手指南:理科背景的思维迁移与实践![]() FPGA学习资源盘点:2026年值得关注的开发板、开源项目与在线社区加载中… |



