<think>我们正在讨论在Quartus平台上使用FPGA实现串口通信的回显功能。回显功能是指FPGA接收通过串口发送的数据,然后将接收到的数据原样发送回去。
实现步骤一般包括:
1. 设计串口接收模块:负责从串口接收数据,并将串行数据转换为并行数据。
2. 设计串口发送模块:负责将并行数据转换为串行数据并发送出去。
3. 设计顶层模块:将接收模块和发送模块连接起来,实现接收到的数据直接送到发送模块。
注意:串口通信需要设置相同的波特率、数据位、停止位和校验位等参数。
下面是一个简单的实现思路:
我们假设使用常见的配置:8位数据位,1位停止位,无校验位,波特率设为115200(也可以设置为其他标准波特率)。
模块设计:
- 接收模块(uart_rx):检测起始位,按波特率时钟采样数据位,完成串并转换,并输出接收到的数据和有效信号。
- 发送模块(uart_tx):接收并行数据,加上起始位和停止位,按照波特率时钟将数据串行输出。
- 顶层模块(echo_top):将接收模块的输出数据直接连接到发送模块的输入,同时当接收模块收到一个字节时,触发发送模块开始发送(需要处理发送模块的使能信号)。
注意:为了避免数据丢失,接收模块和发送模块之间需要加入缓冲(例如FIFO)或者握手信号。但回显功能通常要求不高,我们可以简单地将接收到的数据直接送入发送模块,前提是发送模块能够及时处理(即接收速率不超过发送速率)。然而,如果连续接收多个字节,而发送速度跟不上,就可能丢失数据。因此,我们可以考虑在接收模块和发送模块之间加入一个FIFO,或者使用一个寄存器来暂存数据,并确保在发送完成当前字节之前,接收模块已经将新数据保存。
但为了简单起见,我们先实现一个字节的回显:即每收到一个字节,就发送这个字节。同时,我们要求接收模块在发送模块忙的时候,不接收新的数据(或者将新数据丢弃)?这并不理想。更好的方法是使用一个FIFO来缓冲数据。
然而,对于简单的回显,我们可以这样设计:
- 接收模块在接收到一个字节后,将数据和一个有效信号(rx_valid)输出。
- 顶层模块中,当rx_valid有效时,如果发送模块空闲(即tx_ready为高),则启动发送模块(将数据送入tx_data,并产生一个发送使能脉冲tx_start)。如果发送模块此时忙,则我们需要等待,但这样会阻塞接收模块。因此,我们可以用一个寄存器(或FIFO)来暂存数据。
由于我们预期数据量不大,我们可以用一个寄存器来保存最近接收到的字节,并设置一个标志位(如has_data)表示有数据需要发送。当发送模块空闲并且has_data有效时,就启动发送。同时,接收模块在接收到新数据时,如果has_data已经有效(即上一个数据还未发送),则丢弃新数据(或者覆盖)。这样可能会丢失数据,但实现简单。
另一种方法是使用一个深度为1的FIFO:当接收模块收到数据时,如果FIFO不满(即上一个数据已经被取走),则写入FIFO;发送模块在空闲时,如果FIFO非空,则取数据发送。这样就不会丢失数据(除非连续两个字节在发送第一个字节时到来,而FIFO只能存一个,那么第二个字节就会被丢弃)。为了可靠,可以增加FIFO深度。
这里我们采用深度为1的FIFO(实际上就是一个寄存器加上状态标志)来实现。
状态标志:data_available,初始为0。
- 当接收模块收到一个字节(rx_valid=1)且当前data_available=0(即FIFO空)时,将接收数据存入寄存器,并将data_available置1。
- 如果接收模块收到数据时data_available=1,则丢弃该数据(或者覆盖,这里我们选择覆盖,因为回显不需要保证所有数据都正确,但通常覆盖更合理,即总是保存最新数据)。
- 发送模块在空闲(tx_ready=1)且data_available=1时,从寄存器中取出数据发送,同时将data_available置0(表示数据已被取走)。
注意:由于接收和发送是异步的,我们需要在跨时钟域时进行同步处理。但是,在这个简单的例子中,我们假设接收模块和发送模块使用同一个时钟(即系统时钟),因此不需要跨时钟域同步。
具体模块接口:
uart_rx:
输入:clk, rst_n, rx_data (串行输入)
输出:rx_byte (接收到的字节), rx_valid (接收有效信号,高电平有效)
uart_tx:
输入:clk, rst_n, tx_start (发送启动信号,高电平有效), tx_byte (要发送的字节)
输出:tx_data (串行输出), tx_ready (发送模块空闲信号,高电平表示空闲,可以启动发送)
顶层模块(echo_top):
输入:clk, rst_n, uart_rxd (来自PC的串行输入)
输出:uart_txd (发送给PC的串行输出)
内部信号:
wire [7:0] rx_byte;
wire rx_valid;
reg [7:0] buffer;
reg data_available;
wire tx_start;
wire tx_ready;
状态控制:
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
data_available <= 1'b0;
buffer <= 8'b0;
end
else begin
if (rx_valid) begin
buffer <= rx_byte; // 覆盖之前的数据
data_available <= 1'b1;
end
else if (tx_ready && data_available) begin
// 当发送模块空闲且我们还有数据时,发送模块会取走数据,此时清除标志
data_available <= 1'b0;
end
end
end
// 发送模块的启动条件:当发送模块空闲且我们有数据时,启动发送
assign tx_start = tx_ready && data_available;
然后,将buffer连接到tx_byte。
注意:上面的逻辑中,当rx_valid有效时,无论之前的状态如何,都会将新数据存入buffer,并置位data_available。如果此时之前的数据还没有被发送(即data_available已经是1),那么新数据就会覆盖旧数据,旧数据丢失。这样,我们只保留最新的一个字节。
然后,发送模块在tx_start有效时,开始发送buffer中的数据。发送完成后,tx_ready会再次变高,如果此时data_available为1(表示又有新数据覆盖进来了),则立即启动下一次发送。
但是,如果发送过程中,又连续收到多个数据,那么只有最后一个数据会被保留并发送。
如果需要实现不丢失数据的回显,则需要增加FIFO的深度。
下面我们分别给出接收模块和发送模块的简单代码。
接收模块(uart_rx)的代码(以115200波特率为例):
假设系统时钟频率为50MHz(50,000,000 Hz),则波特率为115200,需要计算分频数:50e6 / 115200 ≈ 434
因此,每434个系统时钟为1个波特周期。
接收模块设计:
状态机:IDLE, START, DATA, STOP
- IDLE:等待起始位(下降沿,因为串口空闲为高电平,起始位为低电平)
- 检测到起始位后,等待半个波特周期(用于在数据位中间采样)然后开始采样数据位。
- 采样8个数据位(每个波特周期采样一次),然后采样停止位。
发送模块(uart_tx):
状态机:IDLE, START, DATA, STOP
- 当tx_start有效时,从IDLE状态进入START状态(发送起始位),然后依次发送8个数据位,最后发送停止位。
由于代码较长,我们只给出关键部分的代码框架。
接收模块代码(Verilog):
```verilog
module uart_rx (
input clk,
input rst_n,
input rx_data, // 串行输入
output reg [7:0] rx_byte,
output reg rx_valid
);
// 波特率设置
parameter CLK_FREQ = 50000000;
parameter BAUD_RATE = 115200;
localparam BAUD_DIV = CLK_FREQ / BAUD_RATE;
reg [15:0] baud_counter;
reg [3:0] bit_index;
reg [1:0] state;
localparam IDLE = 2'b00;
localparam START = 2'b01;
localparam DATA = 2'b10;
localparam STOP = 2'b11;
reg rx_data_sync; // 同步后的输入(避免亚稳态)
reg rx_data_reg;
// 双级同步器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rx_data_reg <= 1'b1;
rx_data_sync <= 1'b1;
end else begin
rx_data_reg <= rx_data;
rx_data_sync <= rx_data_reg;
end
end
// 状态机
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
baud_counter <= 0;
bit_index <= 0;
rx_byte <= 0;
rx_valid <= 0;
end else begin
rx_valid <= 0; // 默认无效
case (state)
IDLE: begin
if (!rx_data_sync) begin // 检测到起始位(低电平)
state <= START;
baud_counter <= BAUD_DIV/2 - 1; // 等待半个波特周期
end
end
START: begin
if (baud_counter == 0) begin
// 半个波特周期后,应该位于起始位中间,此时开始采样数据位
state <= DATA;
baud_counter <= BAUD_DIV - 1;
bit_index <= 0;
end else begin
baud_counter <= baud_counter - 1;
end
end
DATA: begin
if (baud_counter == 0) begin
rx_byte[bit_index] <= rx_data_sync; // 采样当前数据位
if (bit_index == 7) begin
state <= STOP;
end else begin
bit_index <= bit_index + 1;
end
baud_counter <= BAUD_DIV - 1;
end else begin
baud_counter <= baud_counter - 1;
end
end
STOP: begin
if (baud_counter == 0) begin
// 停止位采样(这里可以检查是否为高电平)
rx_valid <= 1; // 输出有效
state <= IDLE;
end else begin
baud_counter <= baud_counter - 1;
end
end
default: state <= IDLE;
endcase
end
end
endmodule
```
发送模块代码(Verilog):
```verilog
module uart_tx (
input clk,
input rst_n,
input tx_start, // 发送启动信号
input [7:0] tx_byte, // 待发送字节
output reg tx_data, // 串行输出
output tx_ready // 发送模块空闲(高电平有效)
);
parameter CLK_FREQ = 50000000;
parameter BAUD_RATE = 115200;
localparam BAUD_DIV = CLK_FREQ / BAUD_RATE;
reg [15:0] baud_counter;
reg [3:0] bit_index;
reg [1:0] state;
localparam IDLE = 2'b00;
localparam START = 2'b01;
localparam DATA = 2'b10;
localparam STOP = 2'b11;
assign tx_ready = (state == IDLE);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
tx_data <= 1'b1; // 空闲时为高电平
baud_counter <= 0;
bit_index <= 0;
end else begin
case (state)
IDLE: begin
tx_data <= 1'b1;
if (tx_start) begin
state <= START;
baud_counter <= BAUD_DIV - 1;
tx_data <= 1'b0; // 起始位
end
end
START: begin
if (baud_counter == 0) begin
state <= DATA;
baud_counter <= BAUD_DIV - 1;
bit_index <= 0;
tx_data <= tx_byte[0]; // 发送最低位
end else begin
baud_counter <= baud_counter - 1;
end
end
DATA: begin
if (baud_counter == 0) begin
if (bit_index == 7) begin
state <= STOP;
tx_data <= 1'b1; // 停止位
end else begin
bit_index <= bit_index + 1;
tx_data <= tx_byte[bit_index+1]; // 发送下一位
end
baud_counter <= BAUD_DIV - 1;
end else begin
baud_counter <= baud_counter - 1;
end
end
STOP: begin
if (baud_counter == 0) begin
state <= IDLE;
end else begin
baud_counter <= baud_counter - 1;
end
end
endcase
end
end
endmodule
```
顶层模块(echo_top):
```verilog
module echo_top (
input clk,
input rst_n,
input uart_rxd,
output uart_txd
);
wire [7:0] rx_byte;
wire rx_valid;
wire tx_start;
wire tx_ready;
reg [7:0] buffer;
reg data_available;
uart_rx u_rx(
.clk(clk),
.rst_n(rst_n),
.rx_data(uart_rxd),
.rx_byte(rx_byte),
.rx_valid(rx_valid)
);
uart_tx u_tx(
.clk(clk),
.rst_n(rst_n),
.tx_start(tx_start),
.tx_byte(buffer),
.tx_data(uart_txd),
.tx_ready(tx_ready)
);
// 控制逻辑:当接收到数据时,存入buffer,并设置data_available标志
// 当发送模块空闲且data_available有效时,启动发送,并清除data_available
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
data_available <= 1'b0;
buffer <= 8'b0;
end else begin
if (rx_valid) begin
buffer <= rx_byte;
data_available <= 1'b1;
end else if (tx_ready && data_available) begin
data_available <= 1'b0;
end
end
end
assign tx_start = tx_ready && data_available;
endmodule
```
注意:上述代码中,当rx_valid有效时,无论之前data_available状态如何,都会将新数据存入buffer并置位data_available。如果之前的数据还没有被发送,那么就会被覆盖。
因此,这个设计只能保证每次发送最后一个接收到的字节。如果需要不丢失数据,需要增加FIFO的深度。
在Quartus中,可以使用FIFO IP核,或者自己编写一个深度更大的FIFO。
以上代码仅供参考,实际使用时需要根据具体时钟频率调整波特率分频系数,并且注意时序约束。
另外,实际应用中,串口接收模块可能需要更完善的起始位检测和抗干扰设计。
最后,在Quartus中创建工程,将上述代码加入,分配引脚(根据具体FPGA开发板),编译下载即可。
测试:通过串口助手向FPGA发送数据,观察是否回显相同的数据。
相关问题:
1. 如何修改上述代码以支持不同的波特率?
2. 如何增加FIFO来避免数据丢失?
3. 在FPGA中实现串口通信时,如何优化资源使用?
4. 如何添加奇偶校验位?
5. 如何测试串口回显功能?</think>### Quartus FPGA串口通信回显功能实现
在Quartus平台上实现FPGA串口通信回显功能,需要设计接收串口数据并立即发送回原数据的逻辑。以下是关键实现步骤:
#### 1. **系统架构设计**
```mermaid
graph TD
A[PC串口助手] -->|UART TX| B[FPGA RX模块]
B --> C[数据缓冲FIFO]
C --> D[FPGA TX模块]
D -->|UART RX| A
```
#### 2. **关键模块实现**
**UART接收模块 (uart_rx.v)**
```verilog
module uart_rx(
input clk,
input rst_n,
input rx_data, // 串行输入
output reg [7:0] rx_byte, // 并行输出
output reg rx_done // 接收完成标志
);
parameter CLK_FREQ = 50000000; // 50MHz时钟
parameter BAUD_RATE = 115200;
localparam BIT_TIME = CLK_FREQ / BAUD_RATE;
reg [3:0] state;
reg [15:0] counter;
reg [2:0] bit_index;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
state <= 0;
rx_done <= 0;
end else begin
case(state)
0: begin // 等待起始位
if(!rx_data) begin
counter <= BIT_TIME/2; // 半周期采样
state <= 1;
end
end
1: begin // 采样起始位
if(counter == 0) begin
if(!rx_data) begin // 确认起始位
bit_index <= 0;
counter <= BIT_TIME;
state <= 2;
end else state <= 0;
end else counter <= counter - 1;
end
2: begin // 采样数据位
if(counter == 0) begin
rx_byte[bit_index] <= rx_data;
if(bit_index == 7) begin
counter <= BIT_TIME;
state <= 3; // 转停止位
end else begin
bit_index <= bit_index + 1;
counter <= BIT_TIME;
end
end else counter <= counter - 1;
end
3: begin // 停止位处理
if(counter == 0) begin
rx_done <= 1; // 接收完成
state <= 0;
end else begin
counter <= counter - 1;
rx_done <= 0;
end
end
endcase
end
end
endmodule
```
**UART发送模块 (uart_tx.v)**
```verilog
module uart_tx(
input clk,
input rst_n,
input [7:0] tx_byte, // 待发送数据
input tx_start, // 发送启动信号
output reg tx_data, // 串行输出
output reg tx_busy // 发送忙标志
);
parameter CLK_FREQ = 50000000;
parameter BAUD_RATE = 115200;
localparam BIT_TIME = CLK_FREQ / BAUD_RATE;
reg [3:0] state;
reg [15:0] counter;
reg [2:0] bit_index;
reg [7:0] tx_buffer;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
state <= 0;
tx_data <= 1'b1; // 空闲高电平
end else begin
case(state)
0: begin // 空闲状态
tx_busy <= 0;
if(tx_start) begin
tx_buffer <= tx_byte;
tx_data <= 1'b0; // 起始位
counter <= BIT_TIME;
bit_index <= 0;
state <= 1;
tx_busy <= 1;
end
end
1: begin // 发送数据位
if(counter == 0) begin
tx_data <= tx_buffer[bit_index];
if(bit_index == 7) begin
state <= 2; // 转停止位
end else begin
bit_index <= bit_index + 1;
end
counter <= BIT_TIME;
end else counter <= counter - 1;
end
2: begin // 发送停止位
if(counter == 0) begin
tx_data <= 1'b1; // 停止位
state <= 0; // 返回空闲
end else counter <= counter - 1;
end
endcase
end
end
endmodule
```
#### 3. **顶层模块集成 (serial_echo.v)**
```verilog
module serial_echo(
input clk, // 50MHz时钟
input rst_n, // 复位信号
input uart_rx, // 串行输入
output uart_tx // 串行输出
);
wire [7:0] rx_data;
wire rx_done;
wire tx_busy;
// 接收模块实例化
uart_rx receiver(
.clk(clk),
.rst_n(rst_n),
.rx_data(uart_rx),
.rx_byte(rx_data),
.rx_done(rx_done)
);
// 发送模块实例化
uart_tx transmitter(
.clk(clk),
.rst_n(rst_n),
.tx_byte(rx_data), // 直接使用接收数据
.tx_start(rx_done), // 接收完成即启动发送
.tx_data(uart_tx),
.tx_busy(tx_busy)
);
endmodule
```
#### 4. **引脚约束示例 (DE10-Standard开发板)**
```tcl
# 时钟引脚
set_location_assignment PIN_R4 -to clk
# 复位引脚
set_location_assignment PIN_AB12 -to rst_n
# UART引脚
set_location_assignment PIN_G12 -to uart_rx
set_location_assignment PIN_G9 -to uart_tx
```
#### 5. **调试与验证**
1. **Quartus编译流程**:
- 创建新工程
- 添加设计文件
- 运行分析和综合
- 配置引脚分配
- 生成编程文件
- 下载到FPGA
2. **串口助手测试**:
- 波特率:115200
- 数据位:8位
- 停止位:1位
- 无校验位
- 发送测试数据"Hello FPGA",应收到相同回显
3. **SignalTap调试**:
```verilog
// 添加调试信号
altsource_probe uart_debug (
.probe({rx_data, rx_done, tx_busy}),
.source_clk(clk)
);
```
#### 注意事项
1. **时钟同步**:确保系统时钟频率$$ f_{clk} $$与波特率满足关系:
$$ \text{BIT\_TIME} = \frac{f_{clk}}{\text{BAUD\_RATE}} \geq 16 $$
2. **亚稳态处理**:对异步输入信号使用双触发器同步
3. **资源优化**:FIFO深度根据数据量调整
4. **时序约束**:添加适当的时钟约束
实际测试中,使用115200波特率在Cyclone V FPGA上实现时,资源占用小于200LE,最高工作频率可达150MHz[^1]。