在剖析了《深入浅出玩转FPGA》的串口代码和IIC控制器代码、xilinx官方的xilinx的iic控制器(参见书《FPGACPLD设计工具──Xilinx ISE使用详解》)、《片上系统设计思想与源代码分析》一书中带有wishbone接口的iic控制器后,本文尝试对以上做一些总结,并分析不同的iic控制器的实现区别。
1、串口
该章节代码来源于《深入浅出玩转FPGA》深入浅出的相关章节。
改代码实现的例子是串口以9600的波特率接受从电脑传来的一个数据,然后立马发回电脑。根据顶层框图可以发现rx的输出数据接口是接在tx的输入接口的。所以这并不是一个完整的全功能的串口,是一个阉割板的仿串口协议的收发,并且没有用状态机实现。
整体框图:
4个文件:
speed_selectspeed_rx(
.clk(clk),//波特率选择模块
.rst_p(rst_p),
.bps_start(bps_start1),
.clk_bps(clk_bps1)
);
my_uart_rxmy_uart_rx(
.clk(clk),//接收数据模块
.rst_p(rst_p),
.rs232_rx(rs232_rx),
.rx_data(rx_data),
.rx_int(rx_int),
.clk_bps(clk_bps1),
.bps_start(bps_start1)
);
///////////////////////////////////////////
speed_selectspeed_tx(
.clk(clk),//波特率选择模块
.rst_p(rst_p),
.bps_start(bps_start2),
.clk_bps(clk_bps2)
);
my_uart_txmy_uart_tx(
.clk(clk),//发送数据模块
.rst_p(rst_p),
.rx_data(rx_data),
.rx_int(rx_int),
.rs232_tx(rs232_tx),
.clk_bps(clk_bps2),
.bps_start(bps_start2)
);
speed_select模块中:
一根输入线受外部限制,在if判断中作为是否有必要启动计时器的标志。定义了一个计时器,将在2603个计时周期处输出改变clk_bps_r变量,作为采样脉冲。同时已经计算好了在一个数据位的正中心采样。我们考虑为9600bps,这样算下来每个数据位的时间是多少,再结合自己板子的时钟,决定应该算多少个周期。
speed模块例化了2次,在接受和发送都可以设置选用不同的波特率。已经定义为变量,只需选择不同的值即可。
my_uart_rx模块中:
input clk; // 50MHz主时钟
input rst_p; //高电平复位信号
input rs232_rx; // RS232接收数据信号
input clk_bps; // clk_bps的高电平为接收或者发送数据位的中间采样点,spped模块就是专门产生这么一个采样脉冲的
output bps_start; //接收到数据后,波特率时钟启动信号置位
output[7:0] rx_data; //接收数据寄存器,保存直至下一个数据来到
output rx_int; //接收数据中断信号,接收到数据期间始终为高电平
clk_bps即为speed模块中传来的采样脉冲信号,bps_start决定是否有必要启动那个计时模块。
进来首先可以看到是对rs232_rx信号的一个滤波:
//----------------------------------------------------------------
reg rs232_rx0,rs232_rx1,rs232_rx2,rs232_rx3; //接收数据寄存器,滤波用
wire neg_rs232_rx; //表示数据线接收到下降沿
always @ (posedge clk or posedge rst_p) begin
if(rst_p) begin
rs232_rx0 <= 1‘b0;
rs232_rx1 <= 1‘b0;
rs232_rx2 <= 1‘b0;
rs232_rx3 <= 1‘b0;
end
else begin //打了4拍
rs232_rx0 <= rs232_rx;
rs232_rx1 <= rs232_rx0;
rs232_rx2 <= rs232_rx1;
rs232_rx3 <= rs232_rx2;
end
end
//因为串口的起始信号是一个下降沿
//参考按键消抖,画图发现可以检测到下降沿,如果正常的一个下降沿可以产生一个时长为20ns的瞬时高脉冲,
//但是如果是20ns-40ns的毛刺就无法产生这个了
//下面的下降沿检测可以滤掉<20ns-40ns的毛刺(包括高脉冲和低脉冲毛刺),
//这里就是用资源换稳定(前提是我们对时间要求不是那么苛刻,因为输入信号打了好几拍)
//(当然我们的有效低脉冲信号肯定是远远大于40ns的)
assign neg_rs232_rx = rs232_rx3 & rs232_rx2 & ~rs232_rx1 & ~rs232_rx0; //接收到下降沿后neg_rs232_rx置高一个时钟周期
可以看到实际上类似与按键去抖,可以自己去画画随着clk演变的图,分析一下对什么样的噪声有效。这是一种常见的处理方式。
根据串口的协议,起始位是一个低电平,所以我们要检测下降沿。
always @ (posedge clk or posedge rst_p)
if(rst_p) begin
bps_start_r <= 1‘bz; //?
rx_int <= 1‘b0;
end
else if(neg_rs232_rx) begin //接收到串口接收线rs232_rx的下降沿标志信号
bps_start_r <= 1‘b1; //启动串口准备数据接收
rx_int <= 1‘b1; //接收数据中断信号使能
end
else if(num==4‘d12) begin //接收完有用数据信息
bps_start_r <= 1‘b0; //数据接收完毕,释放波特率启动信号
rx_int <= 1‘b0; //接收数据中断信号关闭
end
assign bps_start = bps_start_r;
检测到rx上有效的低电平,置位这2个信号,启动计数器模块开始准备技术采样数据,同时表明正在接收数据。当一帧数据完了(12位),则释放。
//----------------------------------------------------------------
reg[7:0] rx_data_r; //串口接收数据寄存器,保存直至下一个数据来到
//----------------------------------------------------------------
reg[7:0] rx_temp_data; //当前接收数据寄存器
always @ (posedge clk or posedge rst_p)
if(rst_p) begin
rx_temp_data <= 8‘d0;
num <= 4‘d0;
rx_data_r <= 8‘d0;
end
else if(rx_int) begin //接收数据处理 这个地方体现出对串口波特率的启动
if(clk_bps) begin //这个地方显然不能把clk_bps放到always的括号里面,因为在speed模块中分析了其实
//读取并保存数据,接收数据为一个起始位,8bit数据,1或2个结束位
num <= num+1‘b1;
case (num)
4‘d1: rx_temp_data[0] <= rs232_rx; //锁存第0bit
4‘d2: rx_temp_data[1] <= rs232_rx; //锁存第1bit
4‘d3: rx_temp_data[2] <= rs232_rx; //锁存第2bit
4‘d4: rx_temp_data[3] <= rs232_rx; //锁存第3bit
4‘d5: rx_temp_data[4] <= rs232_rx; //锁存第4bit
4‘d6: rx_temp_data[5] <= rs232_rx; //锁存第5bit
4‘d7: rx_temp_data[6] <= rs232_rx; //锁存第6bit
4‘d8: rx_temp_data[7] <= rs232_rx; //锁存第7bit
default: ;
endcase
end
else if(num == 4‘d12) begin //我们的标准接收模式下只有1+8+1(2)=11bit的有效数据
num <= 4‘d0; //接收到STOP位后结束,num清零
rx_data_r <= rx_temp_data; //把数据锁存到数据寄存器rx_data中
end
end
assign rx_data = rx_data_r;
rx_int作为这个always中的if的启动信号,按照时钟脉冲锁存数据,当收满一帧(包括奇偶校验和停止位),num清0,传出rx的值。
记住整个过程都是多个并行的always块,是如何实现流程控制的?
即是通过if判断中的是否满足条件来实现的,如:
else if(neg_rs232_rx) begin //等到有效的下降沿启动信号
if(num==4’d12) begin //相当与for循环
if(clk_bps) begin //等待speed模块中的clk计数器到那一刻
tx模块中:
input clk; // 50MHz主时钟
input rst_p; //高电平复位信号
input clk_bps; // clk_bps_r高电平为接收数据位的中间采样点,同时也作为发送数据的数据改变点
input[7:0] rx_data; //接收数据寄存器
input rx_int; //接收数据中断信号,接收到数据期间始终为高电平,在该模块中利用它的下降沿来启动串口发送数据(下降沿表示数据接受完了)
output rs232_tx; // RS232发送数据信号
output bps_start; //接收或者要发送数据,波特率时钟启动信号置位
rx_int是在rx中定义的,还在接受数据则为1,接受完了即为0 ,这个地方即是通过这种信号的传递来实现是先收满一个数据后再发出来一个数据
还是先是一个滤波,可以分析下他是怎么让信号继续保持一个时钟周期的
//---------------------------------------------------------
reg rx_int0,rx_int1,rx_int2; //rx_int信号寄存器,捕捉下降沿滤波用
wire neg_rx_int; // rx_int下降沿标志位
//同样是一个消抖
always @ (posedge clk or posedge rst_p) begin
if(rst_p) begin
rx_int0 <= 1‘b0;
rx_int1 <= 1‘b0;
rx_int2 <= 1‘b0;
end
else begin
rx_int0 <= rx_int;
rx_int1 <= rx_int0;
rx_int2 <= rx_int1;
end
end
assign neg_rx_int = ~rx_int1 & rx_int2; //捕捉到下降沿后,neg_rx_int拉高保持一个主时钟周期
接下来是把数据从rx中传到tx中去
//---------------------------------------------------------
reg[7:0] tx_data; //待发送数据的寄存器
//---------------------------------------------------------
reg bps_start_r;
reg tx_en; //发送数据使能信号,高有效
reg[3:0] num;
always @ (posedge clk or posedge rst_p) begin
if(rst_p) begin
bps_start_r <= 1‘bz;
tx_en <= 1‘b0;
tx_data <= 8‘d0;
end
else if(neg_rx_int) begin //接收数据完毕,准备把接收到的数据发回去
bps_start_r <= 1‘b1;
tx_data <= rx_data; //把接收到的数据存入发送数据寄存器(整个寄存器一个周期就全部赋值过去了?)
tx_en <= 1‘b1; //进入发送数据状态中
end
else if(num==4‘d11) begin //数据发送完成,复位
bps_start_r <= 1‘b0;
tx_en <= 1‘b0;
end
end
assign bps_start = bps_start_r;
思路与rx一致,同样也定义了一个tx_en信号来表征是否发完了。注意这个时候num==11。
接下来吧数据发出去,思路与rx一致:
//---------------------------------------------------------
reg rs232_tx_r;
always @ (posedge clk or posedge rst_p) begin
if(rst_p) begin
num <= 4‘d0;
rs232_tx_r <= 1‘b1;
end
else if(tx_en) begin
if(clk_bps) begin
num <= num+1‘b1;
case (num)
4‘d0: rs232_tx_r <= 1‘b0; //发送起始位
4‘d1: rs232_tx_r <= tx_data[0]; //发送bit0
4‘d2: rs232_tx_r <= tx_data[1]; //发送bit1
4‘d3: rs232_tx_r <= tx_data[2]; //发送bit2
4‘d4: rs232_tx_r <= tx_data[3]; //发送bit3
4‘d5: rs232_tx_r <= tx_data[4]; //发送bit4
4‘d6: rs232_tx_r <= tx_data[5]; //发送bit5
4‘d7: rs232_tx_r <= tx_data[6]; //发送bit6
4‘d8: rs232_tx_r <= tx_data[7]; //发送bit7
4‘d9: rs232_tx_r <= 1‘b1; //发送结束位
default: rs232_tx_r <= 1‘b1;
endcase
end
else if(num==4‘d11) num <= 4‘d0; //复位
end
end
assign rs232_tx = rs232_tx_r;
其实完整梳理下来思路很清晰,没有用状态机依旧实现了串口的功能,后面可以试试如何改为分部的,连续接受后存到一个文件中。
或者把别的部分怎么加进来。
因为不像软件代码的顺序执行,这个是并发执行的,所以还是有差别的。
下面说说testbench,依旧是参见特权的例子:
关于仿真信号很多,所以需要弄懂整个设计思路,再来分析信号的变化。自己之前犯了个低级错误,ml605的板子是200MHZ的,但是自己的`timescale 1ns / 1ps在modelsim中根本产生不了200MHZ的时钟,后面分析计数器的计数值是对的,但是算总的延时时间不对才定位到这里。所以一定要小心,一点点排查,有耐心聚焦,不要一下子看到这么多信号懵逼了。
testbench的原理我就不讲了,信号线应该怎么接,是reg还是wire型。
特权的代码里面封装了一些常见的task:
//-----------------------------------------
//常用信息打印任务封装
//-----------------------------------------
//警告信息打印任务
task warning;
input[2*8:1] msg;
begin
$write("WARNING at %t : %s \n",$time,msg);
end
endtask
//错误信息打印任务
task error;
input[20*8:1] msg;
begin
$write("ERROR at %t:%s \n",$time,msg);
end
endtask
//致命错误打印并停止仿真任务
task fatal;
input[20*8:1] msg;
begin
$write("FATAL at %t : %s",$time,msg);
$write("Simulation false\n");
$stop;
end
endtask
//完成仿真任务
task terminate;
begin
$write("Simulation Successful\n");
$stop;
end
endtask
调用系统命令,最后会在modelsim串口打出来。注意task的调用方法
这里做了遍历测试和随机测试:
//遍历测试
for(cnt=255;cnt>0;cnt=cnt-1) //顺次发送0-255
begin
tx_task(cnt); //发送数据
@(negedge rx_flag); //表示等到这个信号的下降沿再进行下一步。等待接收到的数据
//这个地方是要等串口接收完。185行是一个always块一直在检
//测是否收到数据,里面的rx_flag表示是否收完
if(data_temp ==cnt)
$write("transmit:%d, receive:%d; ture\n",cnt,data_temp); //自收发数据正确
else begin
$write("transmit:%d, receive:%d; error\n",cnt,data_temp); //自收发数据错误
error("false");
end
end
#10_000; //10us延时
//随机测试
for(cnt=1; cnt<255; cnt=cnt+1) //顺次发送0-255
begin
tx_data = {$random};
tx_task(tx_data); //发送随机数据
@(negedge rx_flag); //等待接收到的数据
if(data_temp ==tx_data)
$write("transmit:%d, receive:%d; ture\n",cnt,data_temp); //自收发数据正确
else begin
$write("transmit:%d, receive:%d; error\n",cnt,data_temp); //自收发数据错误
error("false");
end
end
terminate;
end
这里调用了tx_task。注意@(negedge rx_flag);的用法,因为是仿真语句,并不需要可综合。这表示在此处一致等等到rx_flag的下降沿,顺序结构。
//串口发送任务,是主动的,所以只需要一个task,我们主动去调用就可以了
task tx_task;
input[7:0] txdata; //发送数据输入
integer i;
begin
rs232_rx = 0; //起始位
#tx_bps;
for(i=0;i<8;i=i+1) //8位数据发送
begin
rs232_rx = txdata[7-i];
#tx_bps;
end
rs232_rx = 1; //停止位
#tx_bps;
end
endtask
integer j;
//串口接收,是被动的,所以一直要用always块去检测是否有数据发上来
always @(negedge rs232_tx) //起始位检测
begin
#(tx_bps/2);
if(rs232_tx == 0)
begin
rx_flag = 1;
#tx_bps;
for(j=0;j<8;j=j+1)
begin
data_temp[7-j] = rs232_tx;
#tx_bps;
end
rx_flag = 0;
end
end
其实整体思路还是比较清晰的。
与c语言差别很大,如何实现各种结构,需要积累经验。然后就是比较verilog要实现的东西一定要想清楚怎么去实现,划分成那几个功能模块,是否用状态机,内部应定义那些信号来实现各模块间的控制和数据传递。这需要多去联系来增强感觉。
关于iic留到一下讲,更精彩!
lijiuyang
4-28夜于武昌蓝巢逸品