FPGA实现串口与iic控制器总结(1)

在剖析了《深入浅出玩转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夜于武昌蓝巢逸品

时间: 2024-10-07 19:44:15

FPGA实现串口与iic控制器总结(1)的相关文章

FPGA学习笔记之IIC—EEPROM写和读

一.IIC总线协议特点及其工作原理 I2C(Inter-Integrated Circuit)总线是一种由PHILIPS公司开发的两线式串行总线,用于连接微控制器及其外围设备. 1)I2C总线特点 I2C总线最主要的优点是其简单性和有效性.由于接口直接在组件之上,因此I2C总线占用的空间非常小,减少了电路板的空间和芯片管脚的数量,降低了互联成本.总线的长度可高达25英尺,并且能够以10Kbps的最大传输速率支持40个组件. I2C总线的另一个优点是,它支持多主控(multimastering),

FPGA配置方式

首先介绍下AS.PS.JTAG三种模式的区别. AS模式: 烧到FPGA的配置芯片里保存的,FPGA器件每次上电时,作为控制器从配置器件EPCS主动发出读取数据信号,从而把EPCS的数据读入FPGA中,实现对FPGA的编程,该方法适用于不需要经常升级的场合: PS模式:EPCS作为控制器件,把FPGA当做存储器,把数据写人到FPGA中,实现对FPGA的编程.可以采用微控制器(单片机.ARM等)或者CPLD,该模式可以实现对FPGA在线可编程,升级方便: JTAG:直接烧到FPGA里面的,由于是S

MATLAB串口操作和GUI编程

简单的MATLAB GUI编程和串口控制.Word编辑,如需PDF版本,请留言.说实话这个挺难看的……     概述 本文介绍了程序AD9512_Serial_GUI的编程思路和功能.该程序设计到MATLAB的图像用户界面编程的基本方法和串口的基本操作.程序目的在于通过串口写控制字对AD9512进行配置(AD9512通过SPI写入寄存器,本程序只是整个控制程序中的一部分). 修订历史 以下表格展示了本文档的修订过程 日期 版本号 修订内容 2015/01/15 V0.0 初始版本,试验版[1]

四轴飞行器1.2.2 RT-Thread 串口

本来是打算说根据RT-Thread的设备管理提供的驱动接口些串口驱动的,但是仔细一看,我去,串口驱动写好了,只需要调用就可以了.下面我们说说具体怎么使用的.      首先在rt_hw_board_init()函数里面有个rt_hw_usart_init(),这个就是串口初始化的函数了,而且RTT已经写好了三个串口的初始化,只需要修改下宏定义就可以使用,RTT实在用心良苦啊,这都帮我们写好了.      个人的一点看法,可能不是很全面,能力有限.其实我对函数rt_hw_usart_init()的

IIC设备驱动程序

IIC设备是一种通过IIC总线连接的设备,由于其简单性,被广泛引用于电子系统中.在现代电子系统中,有很多的IIC设备需要进行相互之间通信 IIC总线是由PHILIPS公司开发的两线式串行总线,用于连接微处理器和外部IIC设备.IIC设备产生于20世纪80年代,最初专用与音频和视频设备,现在在各种电子设备中都广泛应用 IIC总线有两条总线线路,一条是串行数据线(SDA),一条是串行时钟线(SCL).SDA负责数据传输,SCL负责数据传输的时钟同步.IIC设备通过这两条总线连接到处理器的IIC总线控

Android 驱动(二) IIC简单介绍

一. I2C简单介绍 I2C(Inter-Integrated Circuit)总线是一种由 Philips 公司开发的两线式串行总线,用于连接微控制器及其外围设备.I2C 总线最基本的长处就是简单性和有效性,简单体如今接线简单,仅仅有两根线数据线(SCL)和时钟线(SDA),并且 控制简单.所以一些封装较小的器件多使用I2C总线,常见的使用I2C总线的设备有EEPROM.RTC及一些传感器.这里我们介绍下基于linux的I2C设备驱动的编写. I2C设备驱动的编写有多种方式 一种是直接操作CP

Android 驱动(二) IIC简介

一. I2C简介 I2C(Inter-Integrated Circuit)总线是一种由 Philips 公司开发的两线式串行总线,用于连接微控制器及其外围设备.I2C 总线最主要的优点就是简单性和有效性,简单体现在接线简单,只有两根线数据线(SCL)和时钟线(SDA),而且 控制简单.所以一些封装较小的器件多使用I2C总线,常见的使用I2C总线的设备有EEPROM.RTC及一些传感器.这里我们介绍下基于linux的I2C设备驱动的编写. I2C设备驱动的编写有多种方式 一种是直接操作CPU的I

NetFPGA

From Wikipedia, the free encyclopedia The NetFPGA project[1] is an effort to develop open source hardware and software for rapid prototyping of computer network devices. The project targeted academic researchers, industry users, and students. It was

智能家居系统-软件协议

3. 家庭网关的软件平台 随着嵌入式电子系统越来越复杂,系统软件的稳定性对系统的稳定运行显得愈发重要,在一些功能复杂的系统中,软件的工作量已经超过硬件开发.此时,嵌入式操作系统的作用凸显出来,操作系统可以把开发人员从无尽的软件编码工作中解放出来,只专注于应用开发,而系统资源的管理交与操作系统来完成,这种方式极大地提高了开发效率,缩短了开发周期.同时,基于操作系统的开发可以极大地提高系统的健壮性,各个任务并发执行,各自独立,即时有一个任务程序跑飞,但并不会造成系统的崩溃.另一方面,现代处理器的功能