首先,我们需要明白,UART协议的定义为:接受到1个起始位为0,然后是8个数据位DATA0——7(通常是8个,即8个0/1),1个校验位(可以省略,要求高的时候需要,这里我们将它省略),1个停止位为1;上述说的10个位,也就是说,一个完整的UART协议包是由10个二进制数据组成。
但是我们串口工具接收和发送的数据,都是以十六进制的来表示的,接受器收到的串行数据一定是二进制的,也就是说,我们首先要把每8个二进制数,转化成相应的数据(0——28-1(255)),也就是说,每个UART协议帧,只能发送一个0——255之间的数。串口工具通常会把8位数据,分成两个4位数据,即00——FF。
那么问题来了,我们需要将接收到的单比特的RX信号,转化成8位的RXDATA数据 ,但是在什么时候转换呢?
其次,需要解决波特率和系统时钟的关系,先来看看波特率9600的含义,表示1S内可以传输9600个比特,如果协议帧是上述的10个比特的帧,那就可以传输9600/10 = 960个帧,即1S内可以发送960个0——255之间的数据。
那么这里,可以利用系统时钟和波特率之间的关系来得到一个波特率时钟BaudClk的计数器MAXCOUNT = SYSCLK / BaudRate,每当计数器的Number到达MAXCOUNT,即可以翻转BaudClk信号,这样,我们就获得了一个用于数据采样的BaudClk时钟信号,这里我们以50MHZ的系统时钟和9600波特率为例,得到50000000/9600=5208个CLK,即每个Bit会占据5208个SYSClk,BaudRate频率即为9600HZ。
那么问题来了,在什么时候对RXDATA进行采样呢?带着以上两个问题,我们再回到题目中去看,首先,需要实现的是对RX数据进行延迟处理,因为信号刚输入时处于亚稳态(类似于按键消抖),于是,使用一个RXREG寄存器来保存稳定的RX数据:
那什么时候开始计数呢?应该是刚进入起始位的时候就开始计数,因为起始位和数据位,停止位一样,都要计数5208个Clk.
那么现在,我们得到一个标志位,和一个5208的计数器,RX在每个5208个CLK里面的数据应该是一样的,但最好的采样点,还是最中间的数据。
综上,我们大致可以得到一个期待的数据波形图如下所示:
于是,我们将问题总结一下,这个模块内我们需要解决的问题有如下几个:
1)得到稳定的输入数据,其中主要是为了明显的起始位数据标志位0(类似于按键消抖)。
解决办法为:利用三个寄存器来缓存数据。
代码为:
//使用三个寄存器来对输入信号进行消抖
reg RXReg1,RXReg2,RXReg3;
always @(posedge Clk or negedge Reset_n) begin
if (!Reset_n) begin
RXReg1 <= 1'b1;
RXReg2 <= 1'b1;
RXReg3 <= 1'b1;
end
else begin
RXReg1 <= rx;
RXReg2 <= RXReg1;
RXReg3 <= RXReg2;
end
end//打三个节拍用于消除亚稳态
2)状态定义。
解决办法为:定义状态IDLE(空闲),START(起始位),DATA0——7(8个二进制数据位),STOP(停止位)
代码为三段式状态机:
// 状态转移逻辑,根据当前状态和输入信号确定下一状态
always @(*) begin
case (state)
IDLE: next_state =(RXReg3==0)? START:IDLE;
START: next_state = (Number==CountMax)?DATA0:START; // 转移到数据位状态
DATA0: next_state = (Number==CountMax)?DATA1:DATA0;
DATA1: next_state = (Number==CountMax)?DATA2:DATA1;
DATA2: next_state = (Number==CountMax)?DATA3:DATA2;
DATA3: next_state = (Number==CountMax)?DATA4:DATA3;
DATA4: next_state = (Number==CountMax)?DATA5:DATA4;
DATA5: next_state = (Number==CountMax)?DATA6:DATA5;
DATA6: next_state = (Number==CountMax)?DATA7:DATA6;
DATA7: next_state = (RXReg3&&(Number==CountMax)) ? STOP : DATA7; // 转移到停止位状态
STOP: next_state = (Number==CountMax)?IDLE:STOP; // 转移到空闲状态
default: next_state = IDLE; // 默认转移到空闲状态
endcase
end
// 状态更新逻辑,根据时钟信号更新当前状态
always @(posedge Clk or negedge Reset_n) begin
if (!Reset_n) begin
state <= IDLE;
end
else if (state==IDLE) begin
state <= next_state;
end//在没有进入计数周期的时候,随时可以进入起始标志位
else if (state>0&&Number==CountMax) begin
state <= next_state;
end//在进入计数周期的时候,计数器满才能开始状态转变;
end
// 2个输出逻辑,根据当前状态和计数器输出并行数据和标志位
always @(posedge Clk or negedge Reset_n) begin
if (!Reset_n) begin
data <= 0;
end
else if (state==10) begin
data <= rxData;
end
else begin
data <= data;
end//该句让输出的数据,空闲的时候会输出上一刻的数据,直到下一起始位到来
end
always @(posedge Clk or negedge Reset_n) begin
if (!Reset_n) begin
data_valid <= 0;
end
else if(state == STOP&&Number==0) begin
data_valid <= 1;
end//进入STOP状态即输出一个CLK的标志位
else
data_valid <= 0;
end
3)解决系统时钟和波特率的关系,是模块能适应任何系统时钟和波特率。
解决办法为:定义一个计数器,计数器的最大值就是 系统时钟/波特率,这样,在起始位到停止位之间,会调用10次该计数器,且计数到中间时,会产生一个采样标志位。
代码为:
always @(posedge Clk or negedge Reset_n ) begin
if (!Reset_n) begin
Number <= 0;
CountFlag <= 0;
end
else if (state>0) begin
if(Number==CountMax) begin
Number <= 0;
CountFlag <= 0;
end
else if (Number==CountMax/2) begin
Number <= Number+1;
CountFlag <= 1;
end
else begin
Number <= Number+1;
CountFlag <= 0;
end
end
end
4)并行数据需要暂存在一个8位寄存器RxData里面,何时采集数据?何时停止采集数据?
解决办法为:状态在DATA0——7且采样标志位为1的时候,就会从RxReg3里面采样数据按照状态DATA0——7依次放入RxData [0:7],未采集的数据位数应当保持,在等待期,RxData数据清零。
代码如下:
always @(posedge Clk or negedge Reset_n) begin
if (!Reset_n) begin
rxData <= 0;
end
else if( (CountFlag)&&(state>1&&state<10) ) begin
rxData[state-2] <= RXReg3;
end//状态在1-8里面且采样标志位时才采样
else if (state>1) begin
rxData <= rxData;
end//只要状态进入起始位,rxData都应当保持或采集
else
rxData <= 0;
//这里就是状态处于IDLE的时候,rxdata需要清零
end
至此,我们已经实现了完整的数据接收模块,然后对其进行仿真,仿真后的结果如下图所示(内部变量设置为黄色):
上板验证待发送模块完成后一同测试。
最终测试结果如下:
从图中可以发现,串口能正常的接收和发送数据,使用波形发送软件发送波形数据,在接收端也能正常的收到该波形。
完整代码和测试代码请联系619743936@qq.com