概述
测试环境
操作系统:Windows10
综合仿真:Vivado 2018.3
芯片验证:Zynq7010
串口时序
作为调试交互接口,串口优势非常明显,虽然网上有很多成熟的IP,但作为学习,用Verilog重新写个电路还是很有必要。
我们日常常用的串口配置一般为:
起始位:1bit
数据位:8bit
停止位:1bit
校验位:无
所以今天实现的电路就以这个配置来,固定1起始位8数据位1停止位无校验位。
串口线分为TX和RX,通信时使用交叉线连接,并且必须共地。下图为串口的TX线时序,SCLK为参考时钟。
串口数据是异步传输,因此上图SCLK在实际通信当中不存在,因此通信双方需要约定好这个SCLK速度,才能保证数据通信的正确性,这个约定的时钟就是波特率,baud一般常用有115200、9600等待,115200表示1秒钟之内发送115200位的数据,为了增加实用性,本次设计的电路有一个分频寄存器。
这里以TX发送线为例结合上图简单讲解一下时序。TX线在无数据传输时处于高电平,当TX从高跳变为低时意味着数据开始传输(这个低电平就是起始位),然后根据约定的波特率,从第0个数据位开始传输,当第7个数据位传输完后拉高TX,意味这数据传输完成(这个高电平就是停止位)。
电路实现
模块接口
clock:输入时钟,我这里直接接外部50M有源晶振
reset:串口复位,低电平复位串口
division:分频系数,根据分频系数确定串口baud率
rdata和tdata:接收和发送数据线
rfinish和tfinish:接收和发送完成触发线
ttrigger:发送数据触发线,触发后会发tdata的8位数据通tx发送出去
tx和rx:接外部引脚
参数约定
根据我们的配置,每次我们发送一字节的数据就需要10位,所以本次发送和接收电路都使用状态机实现,共10种状态,约定参数如下:
接收电路实现
接收电路需要根据波特率来读取RX线,因为RX线每次信号在一个串口时钟中部是最稳定的,如下图:
因此我们的逻辑处理时钟必须是串口时钟的2倍才能确定中间位置,再结合division分频系数,可实现如下电路, 此处rfcount_clear在被半个串口时钟周期会发出一个高电平脉冲:
有了串口时钟,我们还需要设计一个接收状态机,方便数据接收,如下图, 因为总共是10种状态, 按照常规来说4位足够, 但由于要中位采样, 所以这里使用5位, 最低位的跳变正好可以用来触发采样:
状态确定后我们即可根据状态来采样RX线上的电平了, 此时使用移位寄存器实现:
处理一些其它的信号,注释都写得很纤细:
以上就是整个串口的接收电路,起始可以看到我并没有使用STATE_IDLE态,因为考虑到连续传输数据,如果把完整的停止位接收下来,会导致rtrigger信号异常。
发送电路的实现
发送电路相较于接收电路来说简单一些,可完全遵从10种状态来实现,思路和接收电路思路差不多,最大的差别是发送电路是把数据往TX线上送,因此可以使用一个电路选择器来实现:
波特率计算
根据我们上边的讲解,我们生成的时钟必须是串口时钟的两倍,因此即便我们输入的是50M的时钟并且不分频,最大的波特率也只能达到25000000,所以可以认为输入时钟进行了二分频,因此计算公式如下:
baud = clock/2/(division+1) => division = clock/2/baud - 1;
例如clock为50MHz, baud为115200,则 division = 50000000/2/115200 - 1 约等于 216
完整代码
/**
* 说明: clock接入后会二分频,串口固定使用1起始位,1结束位,8数据位,无校验位
* 波特率计算公式:baud = clock/2/(division+1) => division = clock/2/baud - 1;
* 例如clock为50MHz, baud为115200,则 division = 50000000/2/115200 - 1 约等于 216
* time: 2021-02-20
* edior: huxiang hello
*/
module uart(
input wire clock, // 时钟输入
input wire reset, // 复位(低电平有效)
input wire [31:0] division, // 分频系数
output wire [7:0] rdata, // 读数据
input wire [7:0] tdate, // 发数据
output wire rfinish, // 收到数据触发线 (高电平触发)
output wire tfinish, // 发送完成触发线 (高电平触发)
input wire ttrigger, // 发送数据更新触发线 (高电平触发)
output reg tx, // 串口tx
input wire rx // 串口rx
);
// ----------------------------- 公用状态符号 ----------------------------------
localparam STATE_START = 4'd0; // 起始
localparam STATE_BIT0 = 4'd1; // 第0位
localparam STATE_BIT1 = 4'd2; // 第1位
localparam STATE_BIT2 = 4'd3; // 第2位
localparam STATE_BIT3 = 4'd4; // 第3位
localparam STATE_BIT4 = 4'd5; // 第4位
localparam STATE_BIT5 = 4'd6; // 第5位
localparam STATE_BIT6 = 4'd7; // 第6位
localparam STATE_BIT7 = 4'd8; // 第7位
localparam STATE_STOP = 4'd9; // 结束
localparam STATE_IDLE = 4'd10; // 空闲
// ----------------------------- 发送部分逻辑 ----------------------------------
// 处理分频计数
reg [32:0] tfcount; // 分频计数 - 最后一位特殊用法,实现clock二分频
wire tfcount_clear; // tfcount清0信号, 高电平脉冲
assign tfcount_clear = (tfcount == {division, 1'b1}) ? 1'b1 : 1'b0;
always @(negedge clock) // 考虑到clock线延迟, 所以使用下降沿触发
begin
if(!reset || tfcount_clear || ttrigger)
tfcount <= 33'd0;
else
tfcount <= tfcount + 33'd1;
end
// 处理发送状态
reg [3:0] tstate; // 发送状态机
wire tx_idle; // tx线空闲
assign tx_idle = (tstate == STATE_IDLE) ? 1'b1 : 1'b0;
always @(posedge clock)
begin
if(!reset)
tstate <= STATE_IDLE; // 默认处于空闲状态
else if(tfcount_clear && !tx_idle)
tstate <= tstate + 4'd1; // 处于发送态
else if(ttrigger)
tstate <= STATE_START; // 切换到起始态
else;
end
// 处理tx线
always @(*) // 这里会综合成多路选择器
case(tstate)
STATE_START: tx = 1'b0;
STATE_BIT0: tx = tdate[0];
STATE_BIT1: tx = tdate[1];
STATE_BIT2: tx = tdate[2];
STATE_BIT3: tx = tdate[3];
STATE_BIT4: tx = tdate[4];
STATE_BIT5: tx = tdate[5];
STATE_BIT6: tx = tdate[6];
STATE_BIT7: tx = tdate[7];
STATE_STOP: tx = 1'b1;
STATE_IDLE: tx = 1'b1;
default: tx = 1'b1;
endcase
// 处理tfinish
reg [3:0] old_tstate;
always @(posedge clock) old_tstate <= tstate;
assign tfinish = (tx_idle && old_tstate==STATE_STOP) ? 1'b1 : 1'b0;
// ----------------------------- 接收部分逻辑 ----------------------------------
// 未确保连续接收正确,接收部分处于STATE_STOP状态时就会发出读数据完成信号
// 处理分频计数
reg [31:0] rfcount; // 分频计数
wire rfcount_clear; // rfcount清0信号, 高电平脉冲
assign rfcount_clear = (rfcount == division) ? 1'b1 : 1'b0;
reg rtrigger; // 开始读信号 (高触发)
always @(negedge clock) // 考虑到clock线延迟, 所以使用下降沿触发
begin
if(!reset || rfcount_clear || rtrigger)
rfcount <= 32'd0;
else
rfcount <= rfcount + 32'd1;
end
// 处理接收状态
reg [4:0] rstate; // 接收状态机 - 最后一位特殊用法, 中间采样
wire rx_idle; // rx空闲时为高电平
assign rx_idle = (rstate[4:1] == STATE_STOP) ? 1'b1 : 1'b0;
always @(posedge clock)
begin
if(!reset)
rstate <= {STATE_STOP,1'b0}; // 默认处于停止状态
else if(rfcount_clear && !rx_idle)
rstate <= rstate + 4'd1; // 处于接收态
else if(rtrigger)
rstate <= {STATE_START,1'b0};// 切换到起始态
else;
end
// 从rx采样电平
reg [7:0] rdata_mov;
always @(posedge rstate[0] or negedge reset) // 综合成移位寄存器
begin
if(!reset)
rdata_mov <= 8'd0;
else
rdata_mov <= {rx, rdata_mov[7:1]};
end
// 数据位连接到rdata
assign rdata = rdata_mov;
// 处理rfinish
reg [3:0] old_rstate;
always @(posedge clock) old_rstate <= rstate[4:1];
assign rfinish = (rx_idle && old_rstate==STATE_BIT7) ? 1'b1 : 1'b0;
// 生成开始读触发信号
always @(posedge clock) rtrigger <= (rx_idle && !rx) ? 1'b1 : 1'b0;// rx线在空闲状态被拉低意味着开始
endmodule
最后
以上就是土豪黑裤为你收集整理的使用verilog设计uart串口的全部内容,希望文章能够帮你解决使用verilog设计uart串口所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复