Zynq7020中UART与PWM的Verilog实现
本文介绍在Zynq-7020平台上使用Verilog HDL从零实现UART和PWM模块的方法,通过硬件逻辑卸载CPU负担,提升系统实时性与稳定性。详细阐述波特率生成、PWM占空比控制、AXI Lite接口集成及软硬件协同设计要点,适用于嵌入式FPGA系统开发。
基于Zynq7020的Uart和PWM的Verilog HDL实现与系统集成
在现代嵌入式设计中,我们常常面临一个矛盾:一方面希望处理器能快速响应外部事件、精确控制硬件;另一方面又不希望CPU被低层次的轮询或定时任务拖垮。特别是在使用如Zynq-7020这类集成了ARM处理器与FPGA逻辑的异构平台时,如何合理划分软硬件职责,成为决定系统性能的关键。
以LED调光和串口通信为例——这两个看似简单的功能,若完全依赖软件实现,轻则占用大量CPU周期,重则因中断延迟导致控制抖动或数据丢失。而如果将它们下沉到可编程逻辑(PL)侧用纯Verilog实现,不仅释放了PS端资源,还能获得纳秒级精度的波形输出和稳定可靠的通信能力。这正是Zynq架构的魅力所在:不是简单地“用FPGA加速”,而是通过 精细的任务解耦 ,让每个部分各司其职。
UART和PWM虽然基础,但它们是通往复杂系统的起点。UART作为调试信息输出、传感器接入甚至协议转换的核心通道,要求高鲁棒性和兼容性;PWM则广泛应用于电机驱动、亮度调节乃至简易音频播放,对时序一致性极为敏感。本文不借助Xilinx IP Catalog中的现成模块,而是从零开始,用标准Verilog HDL构建这两个外设,并探讨其在Zynq-7020上的实际部署方式。
先看UART发送模块的设计思路。它的核心挑战在于 波特率生成的准确性 。假设系统时钟为50MHz,目标波特率为115200bps,则每比特持续时间为约8.68μs,对应分频系数为 50_000_000 / 115200 ≈ 434 。但由于除不尽,实际误差约为0.8%,仍在UART允许的±3%范围内。关键是要避免累积误差影响帧同步。
下面是一个简洁高效的UART发送器实现:
module uart_tx(
input clk,
input rst_n,
input tx_en,
input [7:0] tx_data,
output reg tx
);
parameter CLK_FREQ = 50_000_000;
parameter BAUD = 115200;
localparam DIVIDER = CLK_FREQ / BAUD - 1;
reg [15:0] baud_count;
reg [3:0] bit_count;
reg tx_busy;
wire baud_tick;
// 波特率发生器:计数至DIVIDER后归零,产生tick
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
baud_count <= 0;
else if (baud_count >= DIVIDER)
baud_count <= 0;
else
baud_count <= baud_count + 1;
end
assign baud_tick = (baud_count == DIVIDER);
// 主状态机:起始位 → 数据位 → 停止位
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tx <= 1'b1;
bit_count <= 0;
tx_busy <= 0;
end else begin
if (tx_en && !tx_busy) begin
tx <= 1'b0; // 发送起始位(低电平)
bit_count <= 0;
tx_busy <= 1;
end else if (tx_busy && baud_tick) begin
case (bit_count)
4'd0: tx <= tx_data[0];
4'd1: tx <= tx_data[1];
4'd2: tx <= tx_data[2];
4'd3: tx <= tx_data[3];
4'd4: tx <= tx_data[4];
4'd5: tx <= tx_data[5];
4'd6: tx <= tx_data[6];
4'd7: tx <= tx_data[7];
default: begin
tx <= 1'b1; // 发送停止位
tx_busy <= 0;
end
endcase
bit_count <= bit_count + 1;
end
end
end
endmodule
这段代码虽小,却体现了几个重要的工程考量:
- 使用独立的
tx_busy标志防止重复触发; baud_tick信号确保每次只推进一位,避免因计数器溢出偏差造成错位;- 状态转移集中在单一时钟块内完成,保证时序收敛。
当然,这是一个最简版本。在真实项目中,建议加入FIFO缓冲以支持连续发送,否则主控需严格等待 tx_busy 清零才能写入下一字节,效率较低。此外,接收端更复杂,需要检测下降沿启动采样、采用16倍超采样取中间值判决,还要处理奇偶校验和帧错误判断。
相比之下,PWM的实现要直观得多。其本质就是一个 数值比较器 :在一个固定周期内,当前计数值小于设定阈值时输出高电平,否则为低。这样形成的方波,平均电压正比于占空比。
以下是一个典型的8位边缘对齐PWM模块:
module pwm_gen(
input clk,
input rst_n,
input [7:0] duty_cycle,
output reg pwm_out
);
reg [7:0] counter;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
counter <= 0;
pwm_out <= 0;
end else begin
counter <= counter + 1;
pwm_out <= (counter < duty_cycle) ? 1'b1 : 1'b0;
end
end
endmodule
这个设计利用了自然回滚特性——当 counter 达到255后再加1会自动变为0,形成模256循环。因此PWM频率由公式 $ f_{pwm} = \frac{f_{clk}}{256} $ 决定。例如在50MHz时钟下,输出频率约为195.3kHz,适合用于开关电源或高速LED调光。
但要注意,这种“自由运行+比较”的结构存在一个问题: 占空比更新时机不可控 。如果在计数中途修改 duty_cycle ,可能导致当前周期出现异常脉冲,引发电机震动或灯光闪烁。解决办法是引入双缓冲机制:设置影子寄存器,在计数器归零时统一更新比较值,保证跳变边沿干净。
进一步扩展时,可以共享同一个计数器,驱动多个比较单元,从而实现多路PWM输出。比如三相逆变器所需的互补PWM,只需增加上下桥臂的死区控制逻辑即可。这类高级应用虽不在本文范围,但基础框架已清晰可见。
那么,这些模块如何与Zynq的ARM处理器协同工作?典型架构如下:
+------------------+ AXI Lite +---------------+
| ARM Cortex-A9 |<--------------------->| Register Map |
| (PS - 处理器系统) | | (Memory Mapped)|
+------------------+ +---------------+
|| ||
Interrupt GPIO Out
\/ \/
+------------------+ +------------------+
| UART_RX/TX Logic | | PWM Output Pins |
| (Verilog HDL) | | (to LED/Motor) |
+------------------+ +------------------+
具体流程是:
- 在Vivado中创建Block Design,启用ZYNQ Processing System IP;
- 配置MIO引脚为UART0,用于Linux调试输出;
- 将自定义的UART和PWM模块封装为AXI Lite从设备,挂接到PS的HP或GP接口;
- 分配寄存器地址空间,例如:
-0x43C0_0000: PWM占空比寄存器
-0x43C0_0004: UART发送数据/状态寄存器 - 编译生成比特流并烧录FPGA;
- 在SDK或PetaLinux中编写驱动程序,通过
ioremap()映射物理地址,读写寄存器控制外设。
举个例子,想让LED缓慢呼吸,可以在裸机环境中这样操作:
volatile uint32_t *pwm_reg = (uint32_t *)0x43C00000;
for (int i = 0; i <= 255; i++) {
*pwm_reg = i;
usleep(1000); // 每步延时1ms
}
此时,CPU几乎不参与波形生成过程,仅负责配置参数。真正的PWM信号由FPGA硬逻辑持续输出,不受操作系统调度、中断延迟等影响。即使系统负载飙升,LED亮度依然平稳。
同样,当传感器通过自定义UART上传数据时,也可以选择是否启用中断。比如在接收到完整一帧后拉高一个IRQ信号,通知CPU进行处理。这种方式相比轮询大幅降低功耗,尤其适合电池供电设备。
这种设计解决了许多传统嵌入式开发中的痛点:
- CPU负载过高 ?把周期性任务交给PL,CPU专注业务逻辑;
- 实时性不足 ?硬件PWM边沿抖动远小于定时器中断;
- 原生UART不够用 ?在PL里再“造”几个,成本几乎为零;
- 特殊协议支持难 ?自己写状态机,灵活适配Modbus、DMX512等非标格式。
当然,也有一些细节需要注意:
- 所有时钟应走全局网络(Global Clock Buffer),避免局部布线带来的偏斜;
- 复位信号务必同步化,防止亚稳态传播引发未知行为;
- 对EMI敏感的应用,可改用中心对齐PWM模式,减少谐波噪声;
- PCB布局上,PWM输出线远离模拟信号和晶振,必要时加磁珠滤波;
- 调试阶段强烈建议插入ILA(Integrated Logic Analyzer)核,实时抓取内部信号,比示波器更高效。
最终你会发现,掌握这种“自己造轮子”的能力,远不止于实现两个外设那么简单。它让你真正理解了数字系统的工作原理:从时钟域划分、状态机设计,到总线协议交互、软硬件协同。而这正是高级FPGA工程师与普通使用者之间的分水岭。
未来还可以在此基础上做更多延伸:比如给UART加上DMA引擎,实现千兆级数据吞吐;或者构建带反馈闭环的PWM控制系统,结合ADC采样实现恒流驱动;甚至搭建AXI Stream流水线,用于图像预处理或音频编解码。
Zynq的价值,不在于它有多少现成IP,而在于它提供了一个 可深度定制的计算架构 。当你不再满足于调用API,而是开始思考“能不能让它做得更好”,你就已经踏上了通往系统级设计的大门。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)