【FPGA——Cyclone Ⅳ学习笔记】八.SPI协议和DS1302时钟芯片(EP4CE6F17C8)

本文深入介绍了SPI总线的原理及其在DS1302实时时钟模块上的应用,包括SPI驱动代码解析、DS1302的读写操作流程和实现细节,以及如何通过SPI接口与DS1302交互,实现时间日期的读取和设置。

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

一.原理图

在这里插入图片描述
在这里插入图片描述

二.SPI驱动及代码解释

此实验采用黑金的代码!
和之前IIC介绍相同,先单独介绍SPI的底层驱动模块。具体的SPI协议此处不做详细介绍。
底层的SPI驱动是以标准的四线SPI编写。

1.端口定义、I/O说明和变量声明

module spi_master(
	input                       sys_clk,	
	input                       rst,
	output                      nCS,       //SPI片选输出信号 
	output                      DCLK,      //SPI时钟
	output                      MOSI,      //SPI的MOSI,主机发送
	input                       MISO,      //SPI的MISO,主机接收
	input                       CPOL,		//SPI极性,=0表示时钟空闲低电平;=1表示时钟空闲高电平
	input                       CPHA,		//SPI相位,=0表示第一个边沿触发,=1表示第二个边沿触发
	input                       nCS_ctrl,	//片选控制
	input[15:0]                 clk_div,	//时钟分频
	input                       wr_req,		//写请求触发信号
	output                      wr_ack,		//传输结束反馈信号
	input[7:0]                  data_in,	//要写的数据
	output[7:0]                 data_out	//读到的数据
);

localparam                   IDLE            = 0;		//空闲状态
localparam                   DCLK_EDGE       = 1;		//时钟产生边沿状态
localparam                   DCLK_IDLE       = 2;		//时钟空闲(等待)状态
localparam                   ACK             = 3;		//返回结束信号状态
localparam                   LAST_HALF_CYCLE = 4;		//最后一个边沿状态
localparam                   ACK_WAIT        = 5;

reg                          DCLK_reg;					//暂存时钟输出信号
reg[7:0]                     MOSI_shift;				//暂存要写的数据
reg[7:0]                     MISO_shift;				//暂存读到的数据
reg[2:0]                     state;						//当前状态
reg[2:0]                     next_state;				//下一个状态
reg [15:0]                   clk_cnt;					//时钟等待计数器
reg[4:0]                     clk_edge_cnt;				//时钟边沿计数器

2.寄存值输出

assign MOSI = MOSI_shift[7];							//输出最高位
assign data_out = MISO_shift;							//读到的数据输出
assign nCS = nCS_ctrl;									//片选控制直接由输入转到输出
assign DCLK = DCLK_reg;									//时钟寄存后输出
	
assign wr_ack = (state == ACK);							//获得传输结束信号

3.SPI的CLK配置

此语句块用于产生SPI驱动所需的CLK(时钟线)脉冲信号

//SPI时钟线CLK状态机
always@(posedge sys_clk or posedge rst)
begin
	if(rst)
		state <= IDLE;
	else
		state <= next_state;
end

always@(*)
begin
	case(state)
		IDLE: //SPI空闲状态
			if(wr_req == 1'b1)
				next_state <= DCLK_IDLE;
			else
				next_state <= IDLE;
		DCLK_IDLE: //触发后,时钟先等待clk_div个系统时钟周期
			if(clk_cnt == clk_div)
				next_state <= DCLK_EDGE;
			else
				next_state <= DCLK_IDLE;
		DCLK_EDGE: //SPI时钟线开始产生8个时钟脉冲,每个系统时钟上升沿改变一次SPI时钟电平
			if(clk_edge_cnt == 5'd15)	//当计到第15个边沿时进入下一个状态,此时还差最后一个边沿
				next_state <= LAST_HALF_CYCLE;
			else
				next_state <= DCLK_IDLE;	
		LAST_HALF_CYCLE:	//准备最后一个边沿状态
			if(clk_cnt == clk_div)	//时钟等待clk_div个系统时钟周期
				next_state <= ACK;
			else
				next_state <= LAST_HALF_CYCLE; 	
		ACK: 	//响应状态
			next_state <= ACK_WAIT;
				//等待一个系统时钟周期,确定结束信号
		ACK_WAIT:
			next_state <= IDLE;
		default:
			next_state <= IDLE;
	endcase
end

//SPI时钟线产生时钟脉冲
always@(posedge sys_clk or posedge rst)
begin
	if(rst)
		DCLK_reg <= 1'b0;
	else if(state == IDLE)
		DCLK_reg <= CPOL;				//空闲时时钟恢复空闲极性
	else if(state == DCLK_EDGE)			//SPI时钟线每个系统时钟上升沿改变一次电平,产生时钟脉冲
		DCLK_reg <= ~DCLK_reg;			//相当于系统时钟的2分频
end

//SPI时钟线等待时间计数(传输开始和结束时使用)
always@(posedge sys_clk or posedge rst)
begin
	if(rst)
		clk_cnt <= 16'd0;
	else if(state == DCLK_IDLE || state == LAST_HALF_CYCLE) 
		clk_cnt <= clk_cnt + 16'd1;
	else
		clk_cnt <= 16'd0;
end

//SPI时钟边沿个数计数器
always@(posedge sys_clk or posedge rst)
begin
	if(rst)
		clk_edge_cnt <= 5'd0;
	else if(state == DCLK_EDGE)	//CLK每改变一次电平计数器加1
		clk_edge_cnt <= clk_edge_cnt + 5'd1;
	else if(state == IDLE)
		clk_edge_cnt <= 5'd0;
end

4.读写时序描述

时序图
在这里插入图片描述
读/写数据:要在时钟有效触发边沿到来之前把要读/写的位准备好。

//SPI写数据
always@(posedge sys_clk or posedge rst)
begin
	if(rst)
		MOSI_shift <= 8'd0;
	else if(state == IDLE && wr_req)
		MOSI_shift <= data_in;			//一旦触发立即把要写的数据传递到寄存器
	else if(state == DCLK_EDGE)			//工作在边沿触发状态
		if(CPHA == 1'b0 && clk_edge_cnt[0] == 1'b1)	//第一个边沿触发且第二个边沿刚好到来(因此还未加1)
			MOSI_shift <= {
   
   MOSI_shift[6:0],MOSI_shift[7]};	//准备下一位,把第6高位调到最高位
		else if(CPHA == 1'b1 && (clk_edge_cnt != 5'd0 && clk_edge_cnt[0] == 1'b0)) //每个CLK的第二个边沿
			MOSI_shift <= {
   
   MOSI_shift[6:0],MOSI_shift[7]};
end

//SPI读数据,流程与写数据类似
always@(posedge sys_clk or posedge rst)
begin
	if(rst)
		MISO_shift <= 8'd0;
	else if(state == IDLE && wr_req)
		MISO_shift <= 8'h00;
	else if(state == DCLK_EDGE)
		if(CPHA == 1'b0 && clk_edge_cnt[0] == 1'b0)	//此处的if判断还未理解
			MISO_shift <= {
   
   MISO_shift[6:0],MISO};		//把刚读到的位放到最低位
		else if(CPHA == 1'b1 && (clk_edge_cnt[0] == 1'b1))
			MISO_shift <= {
   
   MISO_shift[6:0],MISO};
end
endmodule 

三.DS1302的读写

DS1302的读写时序图:
在这里插入图片描述

1.ds1302_io.v

此模块是对ds1302的完整地读/写字节过程进行封装(先写地址再读/写数据)。底层是SPI驱动模块。
ds1302采用的实际是三线SPI,即将MISO和MOSI两根线合并,从全双工变成半双工

module ds1302_io(
	input           clk,
	input           rst,
	output          ds1302_ce,			//ds1302的ce片选脚
	output          ds1302_sclk,		//ds1302的时钟管脚
	inout           ds1302_io,			//ds1302的数据管脚
	input           cmd_read,			//读命令
	input           cmd_write,			//写命令
	output          cmd_read_ack,		
	output          cmd_write_ack,
	input[7:0]      read_addr,			//读数据的地址
	input[7:0]      write_addr,			//写数据的地址
	output reg[7:0] read_data,			//读到的数据
	input[7:0]      write_data			//要写的数据
);

localparam S_IDLE         =  0;		//空闲状态
localparam S_CE_HIGH      =  1;		//片选置高状态
localparam S_READ         =  2;		//读操作过渡状态
localparam S_READ_ADDR    =  3;		//写入要读的地址
localparam S_READ_DATA    =  4;		//读数据状态
localparam S_WRITE        =  5

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

默默无闻小菜鸡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值