【数字IC&FPGA项目】AHB_UART-FIFO控制器设计
实现一个带FIFO的UART收发控制器,并挂在AHB接口上,分为AHB接口和控制模块、发送FIFO、UART发送器、接收FIFO、UART接收器、波特率分频器模块:
各部分实现功能:
- UART发送器:从发送FIFO中读取一个字节的数据(8bit),进行并/串转换并发送到TX引脚
- UART接收器:从RX引脚接收异步串行信号,串/并转换为一个字节,打入接收FIFO
- FIFO:采用同步FIFO设计,但是实现效果类似于异步,以TX为例,AHB总线时钟HCLK速率写入FIFO,以波特率读出。这样做的好处时,需要发送数据时,处理器可以用快时钟先将数据打入FIFO作为缓冲,然后UART再慢慢读出,这期间处理器就可以去做别的事情了,不用一直处在等待UART接收数据的阶段。
参考书籍:《ARM Cortex-M0全可编程SoC原理及实现》
1 详细设计
1.1 同步FIFO
参考【数字IC】同步FIFO设计详解(含源码) - 知乎,并做出了一些改进。部分关键代码:
读/写控制逻辑
// 写控制逻辑
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
w_ptr <= 0;
end
else if (wr && !full) begin
w_ptr <= w_ptr + 1;
end
end
// 读控制逻辑
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
r_ptr <= 0;
end
else if (rd && !empty) begin
r_ptr <= r_ptr + 1;
end
end
对Dual-RAM读/写数据
// 直接assign数据到线上,通过使能来控制是否取用该数据
assign r_data = mem_array[r_ptr];
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
for (i = 0; i < 2**AWIDTH; i = i + 1) begin
mem_array[i] <= 0;
end
end
else if (wr && !full) begin
mem_array[w_ptr] <= w_data;
end
end
读写指针比较逻辑(empty/full信号产生逻辑)
这里采取间接比较法,通过保存FIFO中的数据个数elem_cnt产生empty/full信号。
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
elem_cnt <= 0;
end
else begin
// 多个if式的逻辑要判断全面,确保尽量是互斥关系,比如这里不能是wr & !full
if (wr & !rd & !full)
elem_cnt <= elem_cnt + 1;
else if (!wr & rd & !empty)
elem_cnt <= elem_cnt - 1;
else if(wr & rd & !full & !empty)
elem_cnt <= elem_cnt;
else
elem_cnt <= elem_cnt;
end
end
assign empty = (elem_cnt == 0) ? 1 : 0;
assign full = (elem_cnt == 2**AWIDTH) ? 1 : 0;
1.2 uart_rx设计
网上讲解UART的代码很多了,这里只放出个人认为最关键的部分。uart_rx关键代码在于将异步通信信号同步到系统时钟,以及串/并转换状态机的实现。
同步器部分
// 同步器, 通过打拍采样异步信号rx
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
rx_d0 <= 0;
rx_d1 <= 0;
rx_d2 <= 0;
end
else begin
rx_d0 <= uart_rxd;
rx_d1 <= rx_d0;
rx_d2 <= rx_d1;
end
end
// 下降沿捕获
assign start_en = ~rx_d1 & rx_d2 & ~rx_busy;
其中rx_d0
和rx_d1
充当同步器,用来消除亚稳态。rx_d2
和rx_d1
配合用来捕获rx信号的下降沿,即检测起始位。
发送FSM
// FSM
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
state <= idle_st;
b_reg <= 0, count_reg <= 0, data_reg <= 0;
end
else begin
state <= next_state;
b_next <= b_reg; // 波特率计数器, 计满16个为一个波特周期
count_next <= count_reg; // 数据位计数器
data_reg <= data_next; // 寄存8bit待接收的字符
end
end
always @(*) begin
next_state = state;
b_next = b_reg, count_next = count_reg, data_next = data_reg;
uart_rx_done = 0;
case (state)
idle_st: begin
if(start_en) begin
next_state = start_st;
b_next = 0;
end
end
start_st: begin
if(b_tick)
// 从起始位中间计数,确保每次在电平持续中间对数据进行采样
if(b_reg == 4'd7) begin
next_state = data_st;
b_next = 0, count_next = 0;
end
else
b_next = b_reg + 4'd1;
end
data_st: begin
if(b_tick)
if(b_reg == 4'd15) begin
next_state = data_st;
b_next = 0;
data_next = {rx_d2, data_reg[7:1]}; // 采样数据
if (count_next == 8 - 1) // 8个数据位
next_state = stop_st;
else
count_next = count_reg + 3'd1;
end
else
b_next = b_reg + 4'd1;
end
stop_st: begin
if(b_tick)
if(b_reg == 4'd15) begin
next_state = idle_st;
uart_rx_done = 1;
end
else
b_next = b_reg + 4'd1;
end
endcase
end
1.3 uart_tx设计
与uart_rx比较类似,也是通过一个FSM状态机进行数据发送,需要注意的是截止位的处理。没有计满16个周期, 而是提前拉低了1/16个波特率周期,确保没有tx周期略小于rx周期,否则大量数据连续传输时可能会发送丢包
case(state)
idle_st: ...
start_st: ...
data_st: ...
stop_st: //send stop bit
begin
tx_next = 1'b1;
if(b_tick) begin
// 没有计满16个周期, 而是提前拉低了1/16个波特率周期
// 确保没有tx周期略小于rx周期
// 否则大量数据连续传输时可能会发送丢包
if(b_reg == 14) //one stop bit
begin
next_state = idle_st;
uart_tx_done = 1'b1;
end
else
b_next = b_reg + 1;
end
end
...
endcase
1.4 总线接口设计
总线接口一般处理好几件事:
-
内存地址(Memory Map)映射。这里通过判断地址位的最低两位来判断操作
HADDR[7:0] HWRITE 操作 0x00 1 HWDATA[7:0]=>uart_wdata 0x00 0 HRADATA[7:0]<=uart_rdata 0x04 0 HRADATA[7:0]<=status -
判断写数据/写使能
assign uart_wr_en = last_HTRANS[1] & last_HWRITE & last_HSEL & (last_HADDR[7:0] == 8'h00); assign uart_rd_en = last_HTRANS[1] & ~last_HWRITE & last_HSEL & (last_HADDR[7:0] == 8'h00);
写数据时直接采样总线数据到uart_wdata线上, 直到写使能后打入FIFO
assign uart_wdata = HWDATA[7:0];
-
读数据,输出HRDATA和HREADYOUT
下面是整体的代码:
assign HREADYOUT = ~tx_full;
// 写使能
assign uart_wr_en = last_HTRANS[1] & last_HWRITE & last_HSEL & (last_HADDR[7:0] == 8'h00);
// 写数据(一直采样总线数据, 直到写使能后打入FIFO)
assign uart_wdata = HWDATA[7:0];
// 读使能
assign uart_rd_en = last_HTRANS[1] & ~last_HWRITE & last_HSEL & (last_HADDR[7:0] == 8'h00);
// 读出状态/数据
assign HRDATA = (last_HADDR[7:0] == 8'h00) ? {24'b0, uart_rdata} : {24'b0, status};
assign status = {6'b0, tx_full, rx_empty};
// 中断: 收到数据时, 跳转到软件中断服务程序,接收串口数据
assign uart_irq = ~rx_empty;
2 仿真结果
2.1 FIFO部分
对FIFO模块单独进行读写测试:
整体测试:在SoC完成复位、初始化等操作后,开始将字符串"TEST"依次打入发送FIFO
- 处理器选中UART控制器外设所在地址
0x53000000
- 处理器发送’T’所对应的ASCII码
0x00000054
- 写入FIFO(FIFO中的写指针
w_ptr <= w_ptr + 1
),此时empty拉高,通知UART_TX开始从FIFO取数,但是由于UART发送非常慢,因此需要等待tx_done才正式取走FIFO的顶部字符,这期间FIFO的读指针r_ptr是不会变的(否则又将拉低empty导致误触发)。 - 将"TEST"的剩余字符依次打入FIFO
2.2 UART部分
对UART单独进行回环验证测试:
整体测试:
- 从FIFO中读取待发送的字符’T’,寄存在data_reg寄存器中
- 波特率时钟生成:根据波特率周期,对HCLK进行分频产生b_tick,每16个b_tick发送一个bit
- 开始串口发送,发送寄存的数据,data_reg依次右移并发送最低位
- 波特周期计数器count_reg从0~7计数
- count_reg计满后发送停止位,然后拉高tx_done
- 从FIFO中读取并处理"TEST"后面的字符。
2.3 AHB2UART收发器整体测试
对比FIFO相关信号(图中黄色)和UART相关信号(图中蓝色),可以很明显的发现使用FIFO的优势:处理器很快就将数据打入了FIFO,然后就可以去其他的事了。UART再慢慢从FIFO中取用。
需要注意的是尽管这里FIFO的写入和读出速率看似不一样,行为上类似于异步FIFO,但仍然是在一个时钟下控制的(即快时钟HCLK),因此它仍然是一个同步FIFO。UART是通过每发送完一个字符,拉高tx_done来使能FIFO的读操作,控制FIFO的读出,因此波特率是多少,读出的速率就是多少,看似是以波特率来读出数据的。
3 上板验证
复位发送"TEST"到电脑,
然后将进入循环等待模式,将接收数据回环发射。
总结
FIFO通常是用来增加系统的elasticity(弹性)的,也可以简单理解为数据缓冲区。
总的来说,几乎对于任何一个片上系统,AHB总线、UART、FIFO都是基础但不可或缺的外设,作为一个入门的练手项目,还是有很大的收获的。
标签:AHB,begin,last,FPGA,UART,rx,FIFO,uart From: https://blog.csdn.net/CSDN__cyl/article/details/144656883