目录
- 1. RTC简介
- 1.1 IMX6ULL中的RTC
- 1.2 SNVS_LP中的SRTC配置流程
- 1.3 程序实现
- 2. IIC通信协议
- 2.1 IIC基础
- 2.2 IIC通信协议
- 2.2.1 IIC写时序
- 2.2.2 IIC读时序
- 3. IIC通信的硬件框图及配置流程
- 3.1 IMX6ULL的硬件IIC框图
- 3.1 IIC配置流程
- 3.2 硬件IIC代码实现
- 4. SPI通信
- 4.1 SPI通信基础
- 4.1 SPI通信时序
- 4.3 硬件SPI通信模式
- 4.3 硬件SPI配置流程
- 4.4 硬件SPI代码实现
1. RTC简介
RTC(Real-Time Clock)即实时时钟,本质上也属于定时器的一种,不过这个定时器是在单片机断电后,利用外部电源例如纽扣电池,依然能正常运行,其功耗较低,一般由晶体振荡器产生,常见的是32.768khz的晶振,因为这个晶振经过2^15分频后就能得到一个1HZ的时钟信号
,也就是1s;其他的详细功能这里就不再赘述;
1.1 IMX6ULL中的RTC
RTC在IMX6ULL中称为:"SNVS"
:安全的非易失性存储,在有纽扣电池作为后备电源的情况下,不管系统主电源是否断电,SNVS 都正常运行;其RTC的初始时间是:1970-01-01T00:00:00Z
注意事项1
在IMX6ULL中使用RTC:我们要实现的是掉电依然能保持RTC的运行,所以我们使用的是下图的SNVS_LP中的RTC也叫SRTC,而对于SNVS_HP中的RTC我们不进行使用,因为其掉电后数据时间就会清零;因此剩下的配置思路就是和普通定时器类似:
注意事项2:
不要被寄存器手册误导
①、SRTC 计数器是 32 位的,不是 47 位!
②、SNVS_SRTCMR 的 bit14:0 这 15 位是 SRTC 计数器的高 15 位。
③、SNVS_SRTCLR 的 bit31:bit15 这 17 位是 SRTC 计数器的低 17 位;
1.2 SNVS_LP中的SRTC配置流程
上述已经讲述使用SRTC的原因,其配置思路如下:
- 关闭RTC时钟
- 写入新的时间值:年月日时分秒
- 使能RTC时钟
1.3 程序实现
/*时间设置函数*/
void rtc_setdatetime(struct rtc_datetime *datetime)
{
uint64_t seconds =0 ;
/*这里先保存时钟值,方便后序的修改*/
unsigned int tmp = SNVS->LPCR;
/*设置之前要先关闭时钟*/
rtc_disenable();
seconds=rtc_coverdate_to_seconds(datetime);/*得到秒数,年月日转换成秒数*/
SNVS->LPSRTCMR = (unsigned int)(seconds>>17);/*保存高32位*/
SNVS->LPSRTCLR = (unsigned int)(seconds<<15);/*把高位都清零,保存低32位*/
if(tmp&(0x01))/*判断以前是否打开了RTC,如果相等代表是已经打开了RTC,则进行处理*/
{
rtc_enable();
}
}
/*rtc初始化函数*/
/*默认上电运行是1970年1月1日的零时零点,所以要进行配置他的时间*/
void rtc_init(void)
{
/*首先设置的是HPCOMR寄存器*/
struct rtc_datetime rtcData;/*定义一个结构体变量*/
SNVS->HPCOMR |= (1<<31)|(1<<8);/*关闭安全模式,向所有软件开放*/
rtcData.year=2024;
rtcData.month=5;
rtcData.day=10;
rtcData.hour=10;
rtcData.minute=15;
rtcData.second=30;
rtc_setdatetime(&rtcData);
/*开启RTC时钟*/
rtc_enable();
}
2. IIC通信协议
2.1 IIC基础
IIC是串行同步半双工的通信方式,适合短距离的板内通信,最大速率约400Khz,可调整上拉电阻R的值来提高通信速率,R值一般取4.7K或者2.2K,需要两根线SDA数据线和SCL时钟线,可挂载多个设置,但是同一时间只能有一个设备进行通信;
2.2 IIC通信协议
根据通信协议可以直观的了解到IIC通信的过程,其基本包括两部分,一部分是读的时序,一部分是写的时序;同时要了解一下通信的基本术语:
起始位:
就是告诉挂载的设置我要开始发送或者接受信息了,一般就是在SCL高电平的时候SDA来个下降沿信号就代表起始位数据传输:
包含地址信息,与那个设备通信,数据信息等应答信号:
ACK,应答信号就是代表对于任务完成停止位:
与起始位相反,代表通信结束
2.2.1 IIC写时序
对于写时序,其时序图如下:注意的几个点就是SDA的所有bit信号只有在SCL是高电平期间是有效的;其次就发送的命令顺序:
start->ID&W->ACK->R_Adress->ACK->DATA->ACK->STOP
2.2.2 IIC读时序
对于读时序,我们要注意的是主机发送地址码找到要读取的位置,然后发送写的命令,把要读取的寄存器的地址写给从设备,然后再发送从机的地址并附上读的命令,这时候从机就会从对应的寄存器地址取出内容发送给主机,其时序图如下所示,基本和写是类似的,相比写时序而言,就是后面加了一个读的时序;
3. IIC通信的硬件框图及配置流程
对于IIC通信有硬件IIC和软件模拟的IIC,本质都是产生上述的IIC通信时序,对于IMX6ULL而言,有4个硬件IIC,因此这里主要是针对硬件IIC进行配置,大体上就是开启IIC外设时钟,然后配置控制寄存器。和使能相关的位,最后就是封装一个读寄存器和写寄存器的函数;
3.1 IMX6ULL的硬件IIC框图
对于IMX6ULL的硬件IIC的特性如下所示:有标准模式100Khz和高速模式400Khz两种模式;
①、与标准 I2C 总线兼容。
②、多主机运行
③、软件可编程的 64 中不同的串行时钟序列。
④、软件可选择的应答位。
⑤、开始/结束信号生成和检测。
⑥、重复开始信号生成。
⑦、确认位生成。
⑧、总线忙检测
对于IMX6ULL的硬件IIC的框图如下:
3.1 IIC配置流程
这里主要是针对IMX6ULL的硬件IIC的配置流程,如下:
IIC初始化
:包括关闭IIC,设置好分频系数、开启IIC;IIC的读程序编写
:包括发送起始信号,假读,判断是否忙,然后清除标志位,设置接受数据集,对于IMX6ULL而言倒数第二个字节要发送停止ACK的信号,因此如果是要读取一个字节的数据,那么就要读完就立刻发送应答位,对于多个字节的读取,每次都要判断是否忙和清除标志位,并在倒数第二个字节发送应答信号表示读取结束,最后发送停止信号;IIC的写程序的编写
:也是和读类似,发送起始信号,首先判断是否忙,然后清除标志位,后面就是发送数据,每次发送一个字节的数据都要判断是否忙和仲裁检测发送应答位ACK,最后就是清除状态标志位和发送停止位;
3.2 硬件IIC代码实现
/*配置IIC前要关闭IIC*/
/*基础知识点*/
/*初始化IIC,通过输入参数可以直接确定输入的IIC模式*/
void i2c_init(I2C_Type *base)
{
/*关闭IIC*/
base->I2CR &= ~(1<<7);
/*设置频率*/
base->IFDR=0x15;/*640分频:66000000/640=103.125kHZ*/
/*打开IIC,使能IIC*/
base->I2CR|=(1<<7);
}
/* start 信号的产生及从机地址的发送*/
unsigned char i2c_master_start(I2C_Type *base,unsigned char address,enum i2c_direction direction)
{
if(base->I2SR&(1<<5))/*为真的话就是IIC忙*/
{
return 1;/*代表忙*/
}
/*设置为主机模式*/
base->I2CR |= (1<<5)|(1<<4);
/*产生Start信号*/
base->I2DR =((unsigned int)address<<1)|(direction==kI2C_Read?1:0);/*为1的话就是读,为0的话就是写*/
return 0;
}
/*STOP的信号*/
unsigned char i2c_master_stop(I2C_Type *base)
{
unsigned short timeout = 0xffff;
/*清除I2CR的bit5:3*/
base->I2CR &= ~((1<<5)|(1<<4)|(1<<3));
/*等待I2C忙结束*/
while((base->I2SR&(1<<5)))/*为1的话就是一直忙*/
{
timeout--;
if(timeout==0)/*超时跳出*/
{
return I2C_STATUS_TIMEOUT;
}
}
return I2C_STATUS_OK;/*停止信号产生成功*/
}
/*重复发送信号*/
unsigned char i2c_master_repeate_start(I2C_Type *base,unsigned char address,enum i2c_direction direction)
{
/*检测一下IIC是否忙,或者工作在从机模式下*/
if((base->I2SR*(1<<5)) && (((base->I2CR)&(1<<5)) == 0))
{
return 1;
}
/*设置寄存器*/
base->I2CR |= (1<<4)|(1<<2);
/*把器件地址发送过去*/
base->I2DR =((unsigned int)address<<1)|(direction==kI2C_Read?1:0);/*为1的话就是读,为0的话就是写*/
return I2C_STATUS_OK;
}
//错误点1,位移不对,base->I2CR |= (1 << 7); 这里写成了:base->I2CR |= (1 << 4);
unsigned char i2c_check_and_clear_error(I2C_Type *base, unsigned int status)
{
/* 检查是否发生仲裁丢失错误 */
if(status & (1<<4))
{
base->I2SR &= ~(1<<4); /* 清除仲裁丢失错误位 */
base->I2CR &= ~(1 << 7); /* 先关闭I2C */
base->I2CR |= (1 << 7); /* 重新打开I2C */
return I2C_STATUS_ARBITRATIONLOST;
}
else if(status & (1 << 0)) /* 没有接收到从机的应答信号 */
{
return I2C_STATUS_NAK; /* 返回NAK(No acknowledge) */
}
return I2C_STATUS_OK;
}
/*主机对从机进行数据的写入*/
void i2c_master_write(I2C_Type *base,const unsigned char *buff,unsigned int size)
{
/*等待上一个发送完成*/
while(!(base->I2SR&(1<<7)));
/*清除中断*/
base->I2SR &= ~(1<<1);
/*发送数据*/
base->I2CR |= (1<<4);
while(size--)
{
base->I2DR = *buff++;
while(!(base->I2SR &(1<<1)));/*等待数据传输完成*/
base->I2SR &= ~(1<<1);
/*检测ACK*/
if(i2c_check_and_clear_error(base,base->I2SR))
{
break;
}
}
base->I2SR &= ~(1<<1);
i2c_master_stop(base);
}
/*读数据*/
void i2c_master_read(I2C_Type *base,unsigned char *buff,unsigned int size)
{
/*问题程序*/
volatile uint8_t dumpy = 0;/*假读*/
dumpy++;
/*等待传输完成*/
while(!((base->I2SR)&(1<<7)));
/*清除标志位*/
base->I2SR &= ~(1<<1);
/*接受数据*/
base->I2CR &= ~((1<<4)|(1<<3));
if(size == 1)
{
base->I2CR |= (1<<3);/*NACK;倒数第二个数据要发一个不要应答了的信号*/
}
dumpy=base->I2DR;/*假读*/
while(size--)
{
while(!(base->I2SR&(1<<1)));/*等待传输完成*/
base->I2SR &= ~(1<<1); /*清除标志位*/
if(size==0)
{
i2c_master_stop(base);
}
if(size==1)
{
base->I2CR |= (1<<3);/*NACK;倒数第二个要给个应答*/
}
*buff++=base->I2DR;
}
}
unsigned char i2c_master_transfer(I2C_Type *base, struct i2c_transfer *xfer)
{
unsigned char ret = 0;
enum i2c_direction direction = xfer->direction;
base->I2SR &= ~((1 << 1) | (1 << 4)); /* 清除标志位 */
/* 等待传输完成 */
while(!((base->I2SR >> 7) & 0X1)){};
/* 如果是读的话,要先发送寄存器地址,所以要先将方向改为写 */
if ((xfer->subaddressSize > 0) && (xfer->direction == kI2C_Read))
{
direction = kI2C_Write;
}
ret = i2c_master_start(base, xfer->slaveAddress, direction); /* 发送开始信号 */
if(ret)
{
return ret;
}
while(!(base->I2SR & (1 << 1))){}; /* 等待传输完成 */
ret = i2c_check_and_clear_error(base, base->I2SR); /* 检查是否出现传输错误 */
if(ret)
{
i2c_master_stop(base); /* 发送出错,发送停止信号 */
return ret;
}
/* 发送寄存器地址 */
if(xfer->subaddressSize)
{
do
{
base->I2SR &= ~(1 << 1); /* 清除标志位 */
xfer->subaddressSize--; /* 地址长度减一 */
base->I2DR = ((xfer->subaddress) >> (8 * xfer->subaddressSize)); //向I2DR寄存器写入子地址
while(!(base->I2SR & (1 << 1))); /* 等待传输完成 */
/* 检查是否有错误发生 */
ret = i2c_check_and_clear_error(base, base->I2SR);
if(ret)
{
i2c_master_stop(base); /* 发送停止信号 */
return ret;
}
} while ((xfer->subaddressSize > 0) && (ret == I2C_STATUS_OK));
if(xfer->direction == kI2C_Read) /* 读取数据 */
{
base->I2SR &= ~(1 << 1); /* 清除中断挂起位 */
i2c_master_repeate_start(base, xfer->slaveAddress, kI2C_Read); /* 发送重复开始信号和从机地址 */
while(!(base->I2SR & (1 << 1))){};/* 等待传输完成 */
/* 检查是否有错误发生 */
ret = i2c_check_and_clear_error(base, base->I2SR);
if(ret)
{
ret = I2C_STATUS_ADDRNAK;
i2c_master_stop(base); /* 发送停止信号 */
return ret;
}
}
}
/* 发送数据 */
if ((xfer->direction == kI2C_Write) && (xfer->dataSize > 0))
{
i2c_master_write(base, xfer->data, xfer->dataSize);
}
/* 读取数据 */
if ((xfer->direction == kI2C_Read) && (xfer->dataSize > 0))
{
i2c_master_read(base, xfer->data, xfer->dataSize);
}
return 0;
}
4. SPI通信
4.1 SPI通信基础
&emps; SPI通信相对于IIC而言,其通信速率可达几百MH在,因此其再通信速率上的优势是很大的,对于SPI而言其是串行同步全双工的通信方式,其有四根线进行通信,MOSI,MISO,SCL,CS,这四根线;如下图所示:
术语解释,如下:
CS
:片选信号,用来和哪个数据进行通信时进行拉低就行MISO
:也称为:SDI,主机设备接受数据,从机设备发送数据MOSI
:也称为:SDO,主机设备发送数据,从机设备接受数据SCLK
:时钟信号,固定频率
SPI的通信模式一般有四种模式,根据选择时钟极性(CPOL)和相位极性(CPHA)的不同,可以有四种组合,时序如下:我们常用的是CPOL=0,CPHA=0这种模式。
4.1 SPI通信时序
如同UART,IIC有自己的通信协议,对于SPI而言也有自己的通信协议,这个通信协议的实现方式有硬件实现也有软件时间,具体的实现可以根据需求来定,其SPI的通信时序如下:
可以看出其通信的时序非常简单,也就是片选拉低,开始通信,根据时钟极性和相位极性对MOSI和MISO的数据进行读取,这里CPOL=CPHA=0,因此是再时钟的上升沿对数据进行读取,根据上述的时序图可以很好的看出来;
4.3 硬件SPI通信模式
对于IMX6ULL而言,其有硬件SPI,但是片选信号我们可以用自定义的引脚进行拉低实现,也就是片选引脚初始化位GPIO的模式,不采用硬件模式,硬件只负责产生时钟信号和接受发送数据;对于6ULL而言其内部的SPI称为:ECSPI(Enhanced Configurable Serial Peripheral Interface),其实和实际的SPI基本是一样的;ECSPI 有 64*32 个接收FIFO(RXFIFO)和 64*32 个发送 FIFO(TXFIFO)
,ECSPI 特性如下:
①、全双工同步串行接口。
②、可配置的主/从模式。
③、四个片选信号,支持多从机。
④、发送和接收都有一个 32x64 的 FIFO。
⑤、片选信号 SS/CS,时钟信号 SCLK 极性可配置。
⑥、支持 DMA。
4.3 硬件SPI配置流程
SPI的通信是非常简答的,因此对于硬件SPI的配置也比较简单,主要就是要配置其SPI的时钟源,其次就是配置SPI相关的控制寄存器,例如配置好时钟极性,设置好分频系数,对SPI通信进行使能等,另外就是对SPI数据的接受和发送,这个功能是在一个函数中同时完成的,因为SPI是全双工的通信方式,在接受数据的同时也可以发送数据;注意在接受数据时要选好通道并判忙,例如线上有数据时才能进行数据的接受,不然接受的数据就是错误的,因此要进行提前的判断
- 初始化时钟源
- 配置好SPI控制寄存器
- 设置分频系数并使能SPI
- 读取和发送寄存器的控制
4.4 硬件SPI代码实现
其代码实现如下:可以看到代码是非常简短的,相比于IIC通信而言,而且其通信的速率也是非常快的;
#include "bsp_spi.h"
/*初始化*/
void spi_init(ECSPI_Type *base)
{
base->CONREG = 0;/*清零*/
base->CONREG |= (1<<0)|(1<<3)|(1<<4)|(7<<20);/*设置4个位*/
base->CONFIGREG = 0;/*清零*/
base->PERIODREG = 0x2000;
/*设置SPI的时钟*/
base->CONREG &= ~((0xf<<12)|(0xf << 8));
base->CONREG |= (9<<12);/*一级10分频:get :6Mhz*/
}
/*SPI Sent And Receive Function*/
unsigned char spich0_read_write_byte(ECSPI_Type *base,unsigned char txdata)
{
uint32_t spirxdata = 0;
uint32_t spitxdata = txdata;
/*选择通道0,base*/
base->CONREG &= ~(3<<18);
base->CONREG |= (0<<18);
/*数据发送*/
while((base->STATREG & (1<<0))==0);
base->TXDATA=spitxdata;
/*数据接收*/
while((base->STATREG & (1<<3))== 0);
spirxdata = base->RXDATA;
return spirxdata;
}