## 异步收发器UART的Verilog代码 串口[[UART]]是PC和FPGA通信的最简单的方式,它是一种异步串行/全双工的通信方式,尤其是目前的PC都是通过USB端口来进行UART数据的传输,可以实现更高的传输速率,比如1.5Mbps。 --- ### 1. 异步通信的原理 #### 1.1 异步发送 {{ ::serialtxdmodule.gif |}} #### 1.2 异步接收 {{ ::SerialRxDmodule.gif |}} 更多关于UART串行通信的信息,可以阅读[[uart|异步串行通信]] --- #### 1.3 RS-232的串行接口是如何工作的? 一个RS-232接口有如下的特性: * 使用9针的连接器"DB-9" (更老的PCs使用25管脚的"DB-25"),现在都在使用USB-UART的接口方式 * 允许双向全双工通信(PC可以同时接收、发送数据). * 最高的通信速率可以达到10KBytes/s. * DB-9连接器, 它有3根最重要的信号: {{ ::SerialConnector.jpg |}} * 管脚2: RxD(接收数据). * 管脚3: TxD(发送数据). * 管脚5: GND(地). 只需要3根线,就可以进行数据的收、发。 数据通常以多个8位(我们称之为一个Byte)来进行发送,先将其进行串行化:低位(数据的bit 0)先发送,接着是bit 1, ... 最后是最高位的bit 7。 此接口采用异步协议,也就是没有时钟信号与数据一起发送,接收端必须能够对接收到的数据自行进行“定时”提取和判决。 RS-232是这样处理的: - 收、发两端采用事先约定好的同样的通信参数(速率、格式。。。),这要在通信之前手动设定 - 只要是线路闲置,发送端就发出"idle" (="1") - 每一次要发送一个字节的数据,发送端都要先发一个"start" (="0"),这样接收端就可以判别出有一个字节要来了 - 然后发送一个字节的8个位 - 每发完一个字节以后发送端就发"stop" (="1") 我们以0x55来看看是如何发送的 {{ ::SerialCommunication55.gif |}} 一个字节0x55的数值用二进制表示就是01010101,由于先发送最低位(bit-0),线路的变化为: 1-0-1-0-1-0-1-0. 这里有另外一个例子: {{ ::SerialCommunication.gif |}} 传输的数据为0xC4,能不能看出来? 这些位数很难看出来,可以看出,让接收端知道数据的发送速率是非常重要的。 #### 1.4 数据发送能够多快? 发送的速度是以波特(每秒多少个位)来标称的,例如1000波特意味着每秒1000位,或者说每一位持续时间为1毫秒 RS-232接口的传输速率不是任意的,它有一些固定的值: * 1200 bauds. * 9600 bauds. * 38400 bauds. * 115200 bauds (一般来讲这是能用到的最快的速率). 如果传输速率为115200波特,每一位持续时间为(1/115200) = 8.7µs. 如果你要传输8位一个字节的数据,持续时间为8 x 8.7µs = 69µs.但每一个字节还需要额外的开始、停止位,所以你实际需要10 x 8.7µs = 87µs的时间,也就是最大的传输速率为每秒11.5K字节。 物理层 在传输的导线上,信号采用正/负电压的机制: * "1" 使用-10V来发送 (或介于-5V到-15V之间). * "0" 使用+10V来发送 (或介于5V到15V之间). ### 2. 串行接口的Verilog实现 这里我们用115200波特率,FPGA一般运行在更高的频率,远高于115200Hz,我们需要用FPGA的时钟产生每秒115200个脉冲。 传统上RS-232芯片采用1.8432MHz的时钟,可以通过/16分频能够轻松得到115200Hz以及其它波特率的频率。 // 假设FPGA的时钟为1.8432MHz // 我们创建一个4位的计数器 reg [3:0] BaudDivCnt; always @(posedge clk) BaudDivCnt <= BaudDivCnt + 1; // 从0到15计数 // 每16个时钟就会产生一个脉冲信号(每秒115200个脉冲信号) wire BaudTick = (BaudDivCnt==15); 如果时钟不是1.8432MHz,比如2MHz,为产生115200MHz的频率需要分频17.361111111,不是一个整数,解决的方法为时而/17,时而/18,以确保最后得到的平均数为17.361111111,串行接口能够容忍波特率一定的误差范围 - 此方式类似于DDS中任意频率的生成机制。 // 假设FPGA的时钟为2.0000MHz // 使用10位的累加器再附加额外的位用于累加器的进位,总计11位 reg [10:0] acc; // 总计11位 // 每一个时钟累加器增加59 always @(posedge clk) acc <= acc[9:0] + 59; // use 10 bits from the previous accumulator result, but save the full 11 bits result wire BaudTick = acc[10]; // 用最高位作为波特率的时钟输出 在2MHz的情况下, "BaudTick"每秒钟变动115234次,与115200的误差为0.03%。 #### 2.1 参数化的FPGA波特率发生器 下面的设计为25MHz的系统时钟,使用一个16位的累加器,代码可以通过简单地配置一下参数就可以灵活定制,适用不同的系统时钟。 parameter ClkFrequency = 25000000; // 此处为25MHz,使用不同的系统只需要修改这个参数 parameter Baud = 115200; //此处设置波特率为115200,如果使用不同的波特率只需要修改这个参数 parameter BaudGeneratorAccWidth = 16; parameter BaudGeneratorInc = (Baud< 如果系统时钟为12MHz,波特率115200Hz,波特率步进量为629,误差为0.02% 由于计算出来的BaudGeneratorInc结果超出了32位的中间结果,需要做一些调整: parameter BaudGeneratorInc = ((Baud<<(BaudGeneratorAccWidth-4))+(ClkFrequency>>5))/(ClkFrequency>>4); #### 2.2 RS-232的数据发送 异步发送的固定参数:8个数据位,2个停止位,无奇偶校验。 发送端获取8位的数据,将其串行化(当Txd_start信号被断言的时候),当传输发生的时候“busy”信号会被拉高,在此期间“TxD_start”信号被忽略。 采用状态机进行发送比较合适: reg [3:0] state; // the state machine starts when "TxD_start" is asserted, but advances when "BaudTick" is asserted (115200 times a second) always @(posedge clk) case(state) 4'b0000: if(TxD_start) state <= 4'b0100; 4'b0100: if(BaudTick) state <= 4'b1000; // start 4'b1000: if(BaudTick) state <= 4'b1001; // bit 0 4'b1001: if(BaudTick) state <= 4'b1010; // bit 1 4'b1010: if(BaudTick) state <= 4'b1011; // bit 2 4'b1011: if(BaudTick) state <= 4'b1100; // bit 3 4'b1100: if(BaudTick) state <= 4'b1101; // bit 4 4'b1101: if(BaudTick) state <= 4'b1110; // bit 5 4'b1110: if(BaudTick) state <= 4'b1111; // bit 6 4'b1111: if(BaudTick) state <= 4'b0001; // bit 7 4'b0001: if(BaudTick) state <= 4'b0010; // stop1 4'b0010: if(BaudTick) state <= 4'b0000; // stop2 default: if(BaudTick) state <= 4'b0000; endcase 现在,我们只需要产生"TxD"输出。 reg muxbit; always @(state[2:0]) case(state[2:0]) 0: muxbit <= TxD_data[0]; 1: muxbit <= TxD_data[1]; 2: muxbit <= TxD_data[2]; 3: muxbit <= TxD_data[3]; 4: muxbit <= TxD_data[4]; 5: muxbit <= TxD_data[5]; 6: muxbit <= TxD_data[6]; 7: muxbit <= TxD_data[7]; endcase //将起始位、数据位、停止位结合在一起 assign TxD = (state<4) | (state[3] & muxbit); 下面是完整的代码[[async_verilog_source|异步串行通信的Verilog源代码]]也可以直接下载解压使用:{{:async.zip|异步通信的Verilog和C程序完整源代码}}。 #### 2.3 RS-232的数据接收 现在我们来看看数据的接收端。 该模块从RxD线上收集数据。当接收到一个字节时,它出现在“数据”总线上。 一旦接收到一个完整的字节,“data_ready”就会被拉高一个时钟。 注意,只有在断言“ data_ready”时,“ data”才有效。 其余时间,请不要使用它,因为可能会出现新数据,从而使数据混乱。 过采样 异步接收器必须以某种方式与输入信号同步(它通常无法访问发送器使用的时钟)。为了确定何时会有新的数据字节,我们通过以波特率频率的倍数对信号进行过采样来寻找“开始”位。一旦检测到“开始”位,我们就以已知的波特率对线路进行采样以获取数据位。 接收器通常以16倍波特率对输入信号进行过采样。 我们在这里使用了8倍...对于115200波特,采样率为921600Hz。 假设我们有一个可用的“ Baud8Tick”信号,该信号每秒拉高921600次。 具体的设计 首先,传入的“ RxD”信号与我们的时钟无关。我们使用两个D触发器对其进行过采样,并将其同步到我们的时钟域。 reg [1:0] RxD_sync; always @(posedge clk) if(Baud8Tick) RxD_sync <= {RxD_sync[0], RxD}; 我们对数据进行过滤,以免RxD线上的短尖峰误认为起始位。 reg [1:0] RxD_cnt; reg RxD_bit; always @(posedge clk) if(Baud8Tick) begin if(RxD_sync[1] && RxD_cnt!=2'b11) RxD_cnt <= RxD_cnt + 1; else if(~RxD_sync[1] && RxD_cnt!=2'b00) RxD_cnt <= RxD_cnt - 1; if(RxD_cnt==2'b00) RxD_bit <= 0; else if(RxD_cnt==2'b11) RxD_bit <= 1; end 一旦检测到“开始”,状态机就允许我们遍历接收到的每个位。 reg [3:0] state; always @(posedge clk) if(Baud8Tick) case(state) 4'b0000: if(~RxD_bit) state <= 4'b1000; // start bit found? 4'b1000: if(next_bit) state <= 4'b1001; // bit 0 4'b1001: if(next_bit) state <= 4'b1010; // bit 1 4'b1010: if(next_bit) state <= 4'b1011; // bit 2 4'b1011: if(next_bit) state <= 4'b1100; // bit 3 4'b1100: if(next_bit) state <= 4'b1101; // bit 4 4'b1101: if(next_bit) state <= 4'b1110; // bit 5 4'b1110: if(next_bit) state <= 4'b1111; // bit 6 4'b1111: if(next_bit) state <= 4'b0001; // bit 7 4'b0001: if(next_bit) state <= 4'b0000; // stop bit default: state <= 4'b0000; endcase 请注意,我们使用了“ next_bit”信号,从一个位到另一个位。 reg [2:0] bit_spacing; always @(posedge clk) if(state==0) bit_spacing <= 0; else if(Baud8Tick) bit_spacing <= bit_spacing + 1; wire next_bit = (bit_spacing==7); 最后,移位寄存器在数据位到来时收集它们。 reg [7:0] RxD_data; always @(posedge clk) if(Baud8Tick && next_bit && state[3]) RxD_data <= {RxD_bit, RxD_data[7:1]}; 下面是完整的代码[[async_verilog_source|异步串行通信的Verilog源代码]]也可以直接下载解压使用:{{:async.zip|异步通信的Verilog和C程序完整源代码}}。 #### 2.4 使用RS-232发送和接收数据 在这里我们设计允许通过PC(通过PC的串行端口)控制几个FPGA引脚。 它在FPGA(端口名为“ GPout”)上创建8个输出,GPout由FPGA接收的任何字符更新。FPGA上还有8个输入(端口名为“ GPin”)。 每当FPGA接收到字符时,便发送GPin。 GP输出可用于从PC远程控制任何东西,可能是LED或咖啡机...等等 module serialGPIO( input clk, input RxD, output TxD, output reg [7:0] GPout, // general purpose outputs input [7:0] GPin // general purpose inputs ); wire RxD_data_ready; wire [7:0] RxD_data; async_receiver RX(.clk(clk), .RxD(RxD), .RxD_data_ready(RxD_data_ready), .RxD_data(RxD_data)); always @(posedge clk) if(RxD_data_ready) GPout <= RxD_data; async_transmitter TX(.clk(clk), .TxD(TxD), .TxD_start(RxD_data_ready), .TxD_data(GPin)); endmodule