本帖最后由 UT发布 于 2025-4-19 09:55 编辑
软件版本: TD_ 5.6.4 _Release_ 97693
操作系统: WIN11 64bit
硬件平台:适用安路 (Anlogic)FPGA
1概述 本节课继续利用 I2C总线控制器实现对 RTC 时钟芯片 DS1337 的读写访问,进一步验证我们设计的 i2c 控制器的可靠性。有了前面的基础,这节课内容学习起来很轻松。本节课主要是基于我们编写的 I2C 控制器的应用,侧重点是应用,所以不再给出 RTL 级别的仿真结果,直接通过控制器访问 DS1337 芯片。
在完成本实验前,请确保已经完成前面的实验,包括已经掌握以下能力:
1.1 RTC时钟 DS1337 介绍 DS1337是低功耗、两线制串行读写接口、日历和时钟数据按 BCD 码存取的时钟 / 日历芯片。它提供秒、分、小时、星期、日期、月和年等时钟日历数据。另外它还集成了如下几点功能:
( 1) 56 字节掉电时电池保持的 SRAM 数据存储器
引脚 说明:
X1/X2 :标准的 32.768kHz 的 石英晶振 接入端
寄存器地址空间 :
DS1337的寄存器地址空间如下,我们的代码也就是读写 以下地址空间。常用的时间寄存器地址为 00H~06H,用这七个寄存器来存储秒、分、时、星期、日、月、年,通过写相应的寄存器字段来设置和初始化时间 / 日历,寄存器的数据格式以 BCD码表示 ( 4 位 二进制数 来表示 1 位 十进制数 0~9 ) 。
00H 秒 /01H 分寄存器:计数范围为 00~59 , BIT0~BIT3 表示个位。 BIT4~BIT 6表示十位。
02H 小时寄存器 :BIT 6被定义为 12 小时模式 ( “ 1 ”)或 24小时模式 ( “ 0 ”)选择位。在 12小时模式时, 计数范围为 1~12 , BIT 5是 AM ( “ 0 ”)或PM( “ 1 ”)标志位 ;当在 24小时模式时, 计数范围为 00~23 , 需要 6位才能存下 0~23 的数据 ,BIT 5是第二个十位 ( 表示 20 到 23 小时 ) 。
03H 星期寄存器:计数范围为 1~7 , BIT0~BIT3 表示个位。
04H 日寄存器:计数范围为 01~31 , BIT0~BIT3 表示个位, BIT4~BIT5 表示十位。
05H 月寄存器 :BIT7 是世纪位,当年寄存器从 0到 99 溢出时,该位发生变化。
06H 年寄存器:计数范围为 00~99 , BIT0~BIT3 表示个位, BIT4~BIT7 表示十位。
注意: 07H~0FH 为 控制寄存器和状态寄存器 ,用于控制实时时钟、闹钟和方波的输出,本实验并未使用,详细介绍自行查阅芯片手册。
DS1337的读与写 :
写时序很容易理解,和我们前面写 EEPROM一样, 主器件发起开始条件 ,先发送器件地址 和写命令1101000,等待从器件的应答 ,再发送寄存器的地址,等待从器件的应答 ,之后是连续写数据 ,从器件应答,主器件发送结束 信号,代表完成一次写数据过程。
读时序我们采用上图的方式, 主器件发起开始条件 ,先发送器件地址 和写命令11010000 ,等待从器件的应答 ,再发送寄存器的地址,等待从器件的应答 ,然后主器件再重新发送开始条件,再发送从器件的地址和读命令11010001 ,等待从器件的应答 ,接收从器件发来的数据,主器件应答。这种方式比较方便,大家学习 I2C的课程,如果和 ZYNQ 部分 SDK 对比会发现 SDK 里面操作比较麻烦,而且采取的不是这种方式,这是因为 SDK 里面的 I2C 控制不能发送 Repeated Start 位的原因。
我们这里只针对时、分、秒的读写,而且上电后默认的控制寄存器不需要设置,采用默认参数就可以。
1.2 DS1337 时序 根据手册我们可以知道I2C时钟频率、建立时间、保持时间、两次读写之间的时间间隔有没有限制等。 由图可知 DS1337 的 I2C 支持快速和标准模式,串行时钟 SCL 频率可以位于 0~400KHz 。而建立时间 Tsu 和保持时间 Thd 在快速模式下都小于 1us 。 I2C 接口在发送一个 STOP 条件后到发送下一个 START 条件之间的最小间隔时间( Bus Free Time Between a STOP and START Condition )在快速模式( Fast mode )下, 不小于1.3 微秒 、,而在标准模式( Standard mode )下, 不小于4.7 微秒 ,这段时间内总线上必须保持空闲。
1. 3 硬件电路分析 因为 FPGA一般不需要中断功能 ,所以中断管脚和方波编程输出引脚没有使用, 进行悬空处理。 X1、 X2 引脚接入 32.768KHz 的晶振构成一个振荡电路,生成芯片所需要的参考时钟信号。 IIC 总线需要上拉电阻。
2 用户程序设计 本次实验读写 IIC 接口的 RTC 时钟,首先写入一个时间初始值到 RTC 相应的地址空间,再读取时间值, 因为是实时时钟,时钟数据是一直变化的,所以我们需要一直不断的读取时钟的值,并将读取的时间通过串口发送到上位机。实验包含 3 个模块, I 2C MASTER 控制器驱动模块、 UART 发送控制器模块、用户控制模块。 以下给出系统框图,关于 I I C MASTER 控制器驱动、 UART 发送控制器驱动 的详细描述请看前面的实验,我们主要看用户控制模块关于状态机的部分。
2 . 1用户接口时序 先温习下前面课程内容中关于 I2C控制器的功能模块可以接口信号:
I_wr_cnt写数据字节长度,包含了器件地址,发送 I_iic_req前,预设该值
I_rd_cnt读数据字节长度,仅包含读回有效部分,发送 I_iic_req前,预设该值
O_rd_data读出的数据,如果是读请求,当 O_iic_busy从高变低代表数据读回有效
I_iic_req I2C操作请求,根据 I_rd_cnt是否大于 0 决定是否有读请求
I_iic_mode是否支持随机读写,发送 I_iic_req前,预设该值
首先在 O_iic_busy=0即 I2C 总线空闲情况下,设置 I_wr_cnt, I_rd_cnt, I_wr_data,并且设置 I_iic_req=1, 启动 I2C传输。当 O_iic_busy=1说明 I2C 控制器开始传输,这时候可以设置 I_iic_req=0, 结束本次请求,并且等待 O_iic_busy=0, 当 O_iic_busy=0代表本次 传 输结束 .如果发送的是读请求 ( 当 I_rd_cnt>0),则此时 O_rd_data有效可以读走数据。
2 . 2 用户控制状态机 TS_S=0: 板子刚上电时,首先初始化 RTC 寄存器的时间,设置需要访问的寄存器起始地址,并进入到下一个状态;如果已经完成对 RTC 芯片初始化,等待 1000ms 定时使能信号拉高,再次读取 RTC 寄存器的数据。
TS_S=1: 当总线非忙,设置 iic_req=1 ,请求操作 I2C 控制器,并设置需要写入的字节数(此时不需要读操作)。
TS_S=2: 总线忙,重置 iic_req =0 ,此时为写数据过程。
TS_S=3: 等待总线非忙,代表前面写数据已完成,设置 iic_req=1 请求操作 I2C 控制。读操作前需要先写 1BYTE 器件地址, 1BYTE 寄存器起始地址,再读取 3 个时间寄存器。
TS_S=4: 等待总线非忙,表示一次读 RTC 寄存器操作完成。回到状态 0
2 . 3 系统框图 本系统利用数码管显示,实时时钟,用到了前面使用中动态驱动数码管的方案,系统框图如下:
2 . 4 程序源码
module rtc_clock_ds1337#
(
parameter SYSCLKHZ = 25_000_000 //定义系统时钟
)
(
input wire I_sysclk, //系统时钟输入
output wire O_iic_scl, //I2C总线,SCL时钟
inout wire IO_iic_sda, //I2C总线,SDA数据
output wire O_uart_tx //UART串行发送总线
);
localparam T1000MS_CNT = (SYSCLKHZ-1); //定义访问RTC的时间间隔为1000MS
localparam [7:0] RTC_DEV_ADDR = 8'b1101_0000;
reg [8 :0] rst_cnt = 9'd0;//上电延迟复位
reg [29:0] t_cnt = 30'd0;//定时计数器
wire t_en = (t_cnt==T1000MS_CNT);//定时使能
wire [23:0] wr_data;//写数据信号
wire [23:0] rd_data;//读数据信号
wire iic_busy;//I2C总线忙
reg [7 :0] wr_cnt = 8'd0;//写数据计数器
reg [7 :0] rd_cnt = 8'd0;//读数据计数器
reg iic_req = 1'b0;//i2c 控制器请求信号
reg [2 :0] TS_S = 3'd0;//状态机
reg [7 :0] rtc_addr;//RTC的寄存器地址
reg wr_done = 1'b0; //写RTC初值完成信号
//初始化时间的BDC码,12:00:00
wire [7 :0] WSecond = {4'd0,4'd0};//妙
wire [7 :0] WMinute = {4'd0,4'd0};//分
wire [7 :0] WHour = {4'd1,4'd2};//时
reg [23:0] rtime = 24'd0; //用于保存读取的时间,格式为BCD码
assign wr_data = {WHour,WMinute,WSecond};//写数据初值
//**********上电延迟复位***************************/
always@(posedge I_sysclk) begin
if(!rst_cnt[8])
rst_cnt <= rst_cnt + 1'b1;
end
//**********500ms定时计数器**********************/
always@(posedge I_sysclk) begin
if(t_cnt == T1000MS_CNT)
t_cnt <= 0;
else
t_cnt <= t_cnt + 1'b1;
end
//读写RTC时钟芯片状态机
always@(posedge I_sysclk) begin
if(!rst_cnt[8])begin//复位初始化寄存器
rtc_addr <= 8'd0;
iic_req <= 1'b0;
wr_done <= 1'b0;
rd_cnt <= 8'd0;
wr_cnt <= 8'd0;
TS_S <= 2'd0;
end
else begin
case(TS_S)
0:if(wr_done == 1'b0)begin//上电后,wr_done=0,对RTC时间寄存器初始化,给定初始时间
wr_done <= 1'b1;//设置wr_done=1
rtc_addr <= 8'd0;//设置需要访问的寄存器起始地址
TS_S <= 3'd1;//下一个状态
end
else begin //已经对RTC芯片初始化完成
iic_req <= 1'b0; //重置 iic_req =0
if(t_en)//每间隔1000ms进行一次读操作
TS_S <= 3'd3;//下一个状态,进入读寄时间寄存器状态机
end
1:if(!iic_busy)begin//当总线非忙,才可以操作I2C控制器
iic_req <= 1'b1;//请求操作I2C控制器
rd_cnt <= 8'd0;//由于本操作是写数据,不需要读数据,读数据寄存器设置0
wr_cnt <= 8'd5;//需要写入5 BYTES,包括1字节的器件地址,1字节的寄存器起始地址,3字节的BCD时间参数
TS_S <= 3'd2;//下一个状态机
end
2:if(iic_busy)begin//等待总线忙
iic_req <= 1'b0;//重置 iic_req =0
TS_S <= 3'd3;//下一个状态机
end
3:if(!iic_busy)begin//该状态读RTC时间寄存器
iic_req <= 1'b1;//请求操作I2C控制器
rtc_addr <= 8'd0;//读RTC寄存器的起始地址
wr_cnt <= 8'd2;//读操作需要些1BYTE器件地址,1BYTE 寄存器起始地址
rd_cnt <= 8'd3;//读取3个时间寄存器
TS_S <= 3'd4;//下一个状态
end
4:if(iic_busy)begin//等待总线空闲
iic_req <= 1'b0;//重置 iic_req =0
TS_S <= 3'd0;//下一个状态
end
default: TS_S <= 3'd0;//default状态回到0
endcase
end
end
//***********保存从RTC读取到的时间寄存器,时间为BCD格式***********//
always@(posedge I_sysclk) begin
if(!rst_cnt[8])
rtime <=0;
else if(TS_S == 3)
rtime[23: 0] <= rd_data;//读取的时间包括 时:分:秒,BCD格式
end
//例化I2C控制模块
uii2c#
(
.WMEN_LEN(5),//最大支持一次写入4BYTE(包含器件地址)
.RMEN_LEN(3),//最大支持一次读出3BYTE
.CLK_DIV(SYSCLKHZ/100000)//100KHZ I2C总线时钟
)
uii2c_inst
(
.I_clk(I_sysclk),//系统时钟
.I_rstn(rst_cnt[8]),//系统复位
.O_iic_scl(O_iic_scl),//I2C SCL总线时钟
.IO_iic_sda(IO_iic_sda),//I2C SDA数据总线
.I_wr_data({wr_data,rtc_addr,RTC_DEV_ADDR}),//写数据寄存器
.I_wr_cnt(wr_cnt),//需要写的数据BYTES
.O_rd_data(rd_data), //读数据寄存器
.I_rd_cnt(rd_cnt),//需要读的数据BYTES
.I_iic_req(iic_req),//I2C控制器请求
.I_iic_mode(1'b1),//读模式
.O_iic_busy(iic_busy)//I2C控制器忙
//.iic_bus_error(iic_bus_error),//总线错误信号标志
//.IO_iic_sda_dg(IO_iic_sda_dg)//debug IO_iic_sda
);
//以下完成BCD码赚ASCII码,这样通过串口打印可以方便观察
function signed[7:0] ascii ; //定义ascii码转换函数,只需要转换BCD数据
input[7:0] bcd; //输入参数
begin
case(bcd)
0 : ascii = {8'h30};//ascii 码0
1 : ascii = {8'h31};//ascii 码1
2 : ascii = {8'h32};//ascii 码2
3 : ascii = {8'h33};//ascii 码3
4 : ascii = {8'h34};//ascii 码4
5 : ascii = {8'h35};//ascii 码5
6 : ascii = {8'h36};//ascii 码6
7 : ascii = {8'h37};//ascii 码7
8 : ascii = {8'h38};//ascii 码8
9 : ascii = {8'h39};//ascii 码9
default:ascii = {8'h00};
endcase
end
endfunction
//例化UART发送模块
uart_tx_block u_uart_tx_block
(
.I_sysclk(I_sysclk),//系统时钟输入
.O_uart_tx(O_uart_tx),//UART 串行总线数据发送
//高位,8'h0a,8'h0d,为回车+换行控制字符
.I_uart_tx_buf({8'h0a,8'h0d,ascii(rtime[3:0]),ascii(rtime[7:4]),8'h2d,ascii(rtime[11:8]),ascii(rtime[15:12]),8'h2d,ascii(rtime[19:16]),ascii(rtime[23:20])}),
.I_uart_tx_buf_en(t_en)//t_en也是发送使能
);
endmodule 复制代码
3 FPGA工程 米联客的代码管理规范 ,在对应的 FPGA 工程路径下创建 uisrc 路径,并且创建以下文件夹
04_pin:放 fpga 的 pin 脚约束文件或者时序约束文件
05_boot:放编译好的 bit 或者 bin 文件 ( 一般为空 )
4 下载演示 4 . 1硬件连接 (该教程为通用型教程,教程中仅展示一款示例开发板的连接方式,具体连接方式以所购买的开发板型号以及结合配套代码管脚约束为准。)
请确保下载器和开发板已经正确连接,并且开发板已经上电 (注意 JTAG 端子不支持热插拔,而 USB 接口支持,所以在不通电的情况下接通好 JTAG 后,再插入 USB 到电脑,之后再上电,以免造成 JTAG IO 损坏 )
4 . 2运行结果 在配套 demo路径下我们也提供了可以用串口打印的 demo