人生中的第一篇偏技术的文章
这两年主要从事嵌入式应用层的开发,对驱动层了解的不是很透彻,第一篇文章也不知道该写点神魔,那就从几个常用的通信总线开始吧,一方面算是给自己的学习经历添点痕迹(也算是重新学习的过程),二是以后回顾知识点也有个熟悉的资料,方便查阅。以后的内容有自己的想法(可能不对,还请指正,我尽量多查看一下书籍,争取总结正确),也会引用别人的一些见解,我尽量标明出处(链接),防止我断章取义,好让同志们(也可能就我自己看而已,假设有别人浏览吧)查找内容的源头。不BB了,是骡子是马拉出来遛遛吧。
STM32中几种通信方式的区别:
1.UART(USART)
2.IIC总线
3.SPI总线
对博主表示感谢。
UART、SPI、I2C对比
| 对比项 | UART | SPI | I2C |
| 信号线数目 | 3根,RX、TX、GND | 4根,SDO、SDI、SCLK、SS | 2根,SDA、SCLK |
| 设备从属关系 | —— | 存在主从设备。SPI用片选信号选择从机 | 存在主从设备。IIC用地址选择从机。 |
| 通信方式 | 全双工通信 | 全双工通信 | 半双工通信 |
| 通信速率 | 速度慢 | 比I2C总线要快,速度可达到几Mbps | I2C的速度比SPI慢 |
| 应用领域 | 1、UART常用于控制计算机与串行设备的芯片 2、就是我们经常所说的串口,基本都用于调试。 | 主要应用在EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间 | I2C一般是用在同一个板子上的2个IC之间的通信 ,它可以替代标准的并行总线,连接各种集成电路和功能模块。 |
| 传输距离 | I2C需要有双向IO的支持,而且使用上拉电阻,抗干扰能力较弱,一般用于同一板卡上芯片之间的通信,较少用于远距离通信 | ||
| 通信特征 | 异步,一帧可以传5/6/7/8位 | 同步,SPI允许数据一位一位的传送,甚至允许暂停。从最高位开始传。 | 同步,电平信号,一次连续8bit。从最高位开始传 |
| 协议复杂度 | 结构比较复杂 | SPI实现要比UART简单,UART需要固定的波特率,就是说两位数据的间隔要相等,而SPI则无所谓,因为它是有时钟的协议。 | 协议比SPI复杂,但是连线比标准的SPI要少 |
| 对比 | 在点对点的通信中,SPI接口不需要进行寻址操作,且为全双工通信,显得简单高效。 在多个从器件的系统中,每个从器件需要独立的使能信号,硬件上比I2C系统要稍微复杂一些。 |
USART简介:通用同步异步收发器,全双工数据交换,UART,异步通信,为常用外设,用作开发调试。
代码如下:
/**
* @brief USART GPIO 配置,工作参数配置
* @param 无
* @retval 无
*/
void USART_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 打开串口GPIO的时钟
DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);
// 打开串口外设的时钟
DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);
// 将USART Tx的GPIO配置为推挽复用模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);
// 将USART Rx的GPIO配置为浮空输入模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);
// 配置串口的工作参数
// 配置波特率
USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
// 配置 针数据字长
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
// 配置停止位
USART_InitStructure.USART_StopBits = USART_StopBits_1;
// 配置校验位
USART_InitStructure.USART_Parity = USART_Parity_No ;
// 配置硬件流控制
USART_InitStructure.USART_HardwareFlowControl =
USART_HardwareFlowControl_None;
// 配置工作模式,收发一起
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
// 完成串口的初始化配置
USART_Init(DEBUG_USARTx, &USART_InitStructure);
// 串口中断优先级配置
NVIC_Configuration();
// 使能串口接收中断
USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE);
// 使能串口
USART_Cmd(DEBUG_USARTx, ENABLE);
}
/**
* @brief 配置嵌套向量中断控制器NVIC
* @param 无
* @retval 无
*/
static void NVIC_Configuration(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
/* 嵌套向量中断控制器组选择 */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/* 配置USART为中断源 */
NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQ;
/* 抢断优先级*/
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
/* 子优先级 */
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
/* 使能中断 */
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
/* 初始化配置NVIC */
NVIC_Init(&NVIC_InitStructure);
}
// 串口中断服务函数
void DEBUG_USART_IRQHandler(void)
{
uint8_t ucTemp;
if(USART_GetITStatus(DEBUG_USARTx,USART_IT_RXNE)!=RESET)
{
buffer[num] = USART_ReceiveData(DEBUG_USARTx);
// USART_SendData(DEBUG_USARTx,buffer[num]);
num ++;
if(num==10)
{
for(num=0;num<10;num++)
{
Delay(20);//在发送两个数据的间隙需要一定的时间。
Usart_SendByte(DEBUG_USARTx,buffer[num]);
Delay(20);
}
num = 0;
}
}
}
uint16_t USART_ReceiveData(USART_TypeDef* USARTx)//串口接收数据函数,直接读取的USART的数据寄存器。
{
/* Check the parameters */
assert_param(IS_USART_ALL_PERIPH(USARTx));
/* Receive Data */
return (uint16_t)(USARTx->DR & (uint16_t)0x01FF);
}
/***************** 发送一个字节 **********************/
void Usart_SendByte( USART_TypeDef * pUSARTx, uint8_t ch)
{
/* 发送一个字节数据到USART */
USART_SendData(pUSARTx,ch);
/* 等待发送数据寄存器为空 */
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
}
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data)
{
/* Check the parameters */
assert_param(IS_USART_ALL_PERIPH(USARTx));
assert_param(IS_USART_DATA(Data));
/* Transmit Data */
USARTx->DR = (Data & (uint16_t)0x01FF);
}
串口也就这些东西,自从有了固件库,开发就变得很容易了,只要懂得原理,代码不需要写太多,只要将一些问题想全面,考虑上一些特殊的情况,软件上做一些保护尽量避免发生问题。
SPI简介:高速全双工的串行总线。

(1)四条信号线:串行时钟(SCLK)、串行数据输出(SDO)、串行数据输入(SDI)、片选线(SS)。(所谓的进出,是针对信号进出主机而言)
(2)SPI总线可以实现多SPI设备互相连接。提供时钟的SPI设备为主设备(Master),其他设备为从设备(Slave)。SCLK信号线只由主设备控制,从设备不能控制信号线。
(3)在SPI总线上,某一时刻可以出现多个从机,但只能存在一个主机。主机通过片选线来确定要通信的从机。这就要求从机的MISO口具有三态特性,使得该口线在器件未被选通时表现为高阻抗。
(4)主从设备间可以实现全双工通信,收发独立,操作简单,数据传输速率较高,但需要占用主机较多的口线(每个从机都需要一根片选线),而且只支持单个主机,没有指定的流控制,没有应答机制确认是否接收到数据。
(5)数据输出通过SDO线,数据在时钟上沿或下沿时改变(即发送),在紧接着的下沿或上沿被读取,从而完成一位数据传输。数据输入也使用同样原理。因此,8位数据的传输,至少需要8次时钟信号的改变(上沿和下沿为一次)。
(6)普通的串行通讯一次连续传送至少8位数据,而SPI允许数据一位一位的传送,甚至允许暂停,因为SCK时钟线由主控设备控制,当没有时钟跳变时,从设备不采集或传送数据。也就是说,主设备通过对SCK时钟线的控制可以完成对通讯的控制。
(7)SPI接口在CPU和外围低速器件之间进行同步串行数据传输,在主器件的移位脉冲下,数据按位传输,高位在前(先传?),低位在后,为全双工通信。
2、数据传输
(1)SPI在数据传输的时候,需要确定两件事情:
- 其一,数据是在时钟的上升沿采集还是下降沿采集;
- 其二,时钟的初始(空闲)状态是为高电平还是低电平。
- 对比:I2C空闲状态时,时钟线(不是数据线?)为高电平,数据采集时,时钟线也为高电平,但SPI给出了更自由的方式。
(2)两个概念
CPOL:时钟极性,表示SPI在空闲时,时钟信号是高电平还是低电平。
CPHA:时钟相位,表示SPI设备是在在时钟的上升沿采集还是下降沿采集。
则SPI数据传输就有四种可能---按照标准的说法,SPI数据传输就有四种模式。
(3)四种模式
| 模式 | CPOL | CPHA |
| 0 | 0 | 0 |
| 1 | 0 | 1 |
| 2 | 1 | 0 |
| 3 | 1 | 1 |
模式0(杠cs表示片选信号)

模式1

模式2

模式3

代码如下:
/**
* @brief SPI_FLASH初始化
* @param 无
* @retval 无
*/
void SPI_FLASH_Init(void)
{
SPI_InitTypeDef SPI_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
/* 使能SPI时钟 */
FLASH_SPI_APBxClock_FUN ( FLASH_SPI_CLK, ENABLE );
/* 使能SPI引脚相关的时钟 */
FLASH_SPI_CS_APBxClock_FUN ( FLASH_SPI_CS_CLK|FLASH_SPI_SCK_CLK|
FLASH_SPI_MISO_PIN|FLASH_SPI_MOSI_PIN, ENABLE );
/* 配置SPI的 CS引脚,普通IO即可 */
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_CS_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(FLASH_SPI_CS_PORT, &GPIO_InitStructure);
/* 配置SPI的 SCK引脚*/
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_SCK_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(FLASH_SPI_SCK_PORT, &GPIO_InitStructure);
/* 配置SPI的 MISO引脚*/
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MISO_PIN;
GPIO_Init(FLASH_SPI_MISO_PORT, &GPIO_InitStructure);
/* 配置SPI的 MOSI引脚*/
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MOSI_PIN;
GPIO_Init(FLASH_SPI_MOSI_PORT, &GPIO_InitStructure);
/* 停止信号 FLASH: CS引脚高电平*/
SPI_FLASH_CS_HIGH();
/* SPI 模式配置 */
// FLASH芯片 支持SPI模式0及模式3,据此设置CPOL CPHA
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(FLASH_SPIx , &SPI_InitStructure);
/* 使能 SPI */
SPI_Cmd(FLASH_SPIx , ENABLE);
}
/* 获取 SPI Flash ID */
FlashID = SPI_FLASH_ReadID();
函数主体:
/**
* @brief 读取FLASH ID
* @param 无
* @retval FLASH ID
*/
#define Dummy_Byte 0xFF
#define W25X_JedecDeviceID 0x9F
u32 SPI_FLASH_ReadID(void)
{
u32 Temp = 0, Temp0 = 0, Temp1 = 0, Temp2 = 0;
/* 开始通讯:CS低电平 */
SPI_FLASH_CS_LOW();
/* 发送JEDEC指令,读取ID */
SPI_FLASH_SendByte(W25X_JedecDeviceID);
/* 读取一个字节数据 */
Temp0 = SPI_FLASH_SendByte(Dummy_Byte);
/* 读取一个字节数据 */
Temp1 = SPI_FLASH_SendByte(Dummy_Byte);
/* 读取一个字节数据 */
Temp2 = SPI_FLASH_SendByte(Dummy_Byte);
/* 停止通讯:CS高电平 */
SPI_FLASH_CS_HIGH();
/*把数据组合起来,作为函数的返回值*/
Temp = (Temp0 << 16) | (Temp1 << 8) | Temp2;
return Temp;
}
/**
* @brief 使用SPI发送一个字节的数据
* @param byte:要发送的数据
* @retval 返回接收到的数据
*/
u8 SPI_FLASH_SendByte(u8 byte)
{
SPITimeout = SPIT_FLAG_TIMEOUT;
/* 等待发送缓冲区为空,TXE事件 */
while (SPI_I2S_GetFlagStatus(FLASH_SPIx , SPI_I2S_FLAG_TXE) == RESET)
{
if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0);
}
/* 写入数据寄存器,把要写入的数据写入发送缓冲区 */
SPI_I2S_SendData(FLASH_SPIx , byte);
SPITimeout = SPIT_FLAG_TIMEOUT;
/* 等待接收缓冲区非空,RXNE事件 */
while (SPI_I2S_GetFlagStatus(FLASH_SPIx , SPI_I2S_FLAG_RXNE) == RESET)
{
if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(1);
}
/* 读取数据寄存器,获取接收缓冲区数据 */
return SPI_I2S_ReceiveData(FLASH_SPIx );
}
这样就可以获取FLASH的ID了,通过ID的读取操作可以检测FLASH连接是否稳妥。
/* 擦除将要写入的 SPI FLASH 扇区,FLASH写入前要先擦除 */
// 这里擦除4K,即一个扇区,擦除的最小单位是扇区
SPI_FLASH_SectorErase(0x00);
/**
* @brief 擦除FLASH扇区
* @param SectorAddr:要擦除的扇区地址
* @retval 无
*/
void SPI_FLASH_SectorErase(u32 SectorAddr)
{
/* 发送FLASH写使能命令 */
SPI_FLASH_WriteEnable();
SPI_FLASH_WaitForWriteEnd();
/* 擦除扇区 */
/* 选择FLASH: CS低电平 */
SPI_FLASH_CS_LOW();
/* 发送扇区擦除指令*/
SPI_FLASH_SendByte(W25X_SectorErase);
/*发送擦除扇区地址的高位*/
SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16);
/* 发送擦除扇区地址的中位 */
SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8);
/* 发送擦除扇区地址的低位 */
SPI_FLASH_SendByte(SectorAddr & 0xFF);
/* 停止信号 FLASH: CS 高电平 */
SPI_FLASH_CS_HIGH();
/* 等待擦除完毕*/
SPI_FLASH_WaitForWriteEnd();
}
/**
* @brief 等待WIP(BUSY)标志被置0,即等待到FLASH内部数据写入完毕
* @param none
* @retval none
*/
void SPI_FLASH_WaitForWriteEnd(void)
{
u8 FLASH_Status = 0;
/* 选择 FLASH: CS 低 */
SPI_FLASH_CS_LOW();
/* 发送 读状态寄存器 命令 */
SPI_FLASH_SendByte(W25X_ReadStatusReg);
/* 若FLASH忙碌,则等待 */
do
{
/* 读取FLASH芯片的状态寄存器 */
FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte);
}
while ((FLASH_Status & WIP_Flag) == SET); /* 正在写入标志 */
/* 停止信号 FLASH: CS 高 */
SPI_FLASH_CS_HIGH();
}
/**
* @brief 对FLASH按页写入数据,调用本函数写入数据前需要先擦除扇区
* @param pBuffer,要写入数据的指针
* @param WriteAddr,写入地址
* @param NumByteToWrite,写入数据长度,必须小于等于SPI_FLASH_PerWritePageSize
* @retval 无
*/
void SPI_FLASH_PageWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
/* 发送FLASH写使能命令 */
SPI_FLASH_WriteEnable();
/* 选择FLASH: CS低电平 */
SPI_FLASH_CS_LOW();
/* 写页写指令*/
SPI_FLASH_SendByte(W25X_PageProgram);
/*发送写地址的高位*/
SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);
/*发送写地址的中位*/
SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);
/*发送写地址的低位*/
SPI_FLASH_SendByte(WriteAddr & 0xFF);
if(NumByteToWrite > SPI_FLASH_PerWritePageSize)
{
NumByteToWrite = SPI_FLASH_PerWritePageSize;
FLASH_ERROR("SPI_FLASH_PageWrite too large!");
}
/* 写入数据*/
while (NumByteToWrite--)
{
/* 发送当前要写入的字节数据 */
SPI_FLASH_SendByte(*pBuffer);
/* 指向下一字节数据 */
pBuffer++;
}
/* 停止信号 FLASH: CS 高电平 */
SPI_FLASH_CS_HIGH();
/* 等待写入完毕*/
SPI_FLASH_WaitForWriteEnd();
}
/**
* @brief 对FLASH写入数据,调用本函数写入数据前需要先擦除扇区
* @param pBuffer,要写入数据的指针
* @param WriteAddr,写入地址
* @param NumByteToWrite,写入数据长度
* @retval 无
*/
void SPI_FLASH_BufferWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
u8 NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;
/*mod运算求余,若writeAddr是SPI_FLASH_PageSize整数倍,运算结果Addr值为0*/
Addr = WriteAddr % SPI_FLASH_PageSize;
/*差count个数据值,刚好可以对齐到页地址*/
count = SPI_FLASH_PageSize - Addr;
/*计算出要写多少整数页*/
NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
/*mod运算求余,计算出剩余不满一页的字节数*/
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
/* Addr=0,则WriteAddr 刚好按页对齐 aligned */
if (Addr == 0)
{
/* NumByteToWrite < SPI_FLASH_PageSize */
if (NumOfPage == 0)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
}
else /* NumByteToWrite > SPI_FLASH_PageSize */
{
/*先把整数页都写了*/
while (NumOfPage--)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
WriteAddr += SPI_FLASH_PageSize;
pBuffer += SPI_FLASH_PageSize;
}
/*若有多余的不满一页的数据,把它写完*/
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
}
}
/* 若地址与 SPI_FLASH_PageSize 不对齐 */
else
{
/* NumByteToWrite < SPI_FLASH_PageSize */
if (NumOfPage == 0)
{
/*当前页剩余的count个位置比NumOfSingle小,一页写不完*/
if (NumOfSingle > count)
{
temp = NumOfSingle - count;
/*先写满当前页*/
SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
WriteAddr += count;
pBuffer += count;
/*再写剩余的数据*/
SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp);
}
else /*当前页剩余的count个位置能写完NumOfSingle个数据*/
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
}
}
else /* NumByteToWrite > SPI_FLASH_PageSize */
{
/*地址不对齐多出的count分开处理,不加入这个运算*/
NumByteToWrite -= count;
NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
/* 先写完count个数据,为的是让下一次要写的地址对齐 */
SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
/* 接下来就重复地址对齐的情况 */
WriteAddr += count;
pBuffer += count;
/*把整数页都写了*/
while (NumOfPage--)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
WriteAddr += SPI_FLASH_PageSize;
pBuffer += SPI_FLASH_PageSize;
}
/*若有多余的不满一页的数据,把它写完*/
if (NumOfSingle != 0)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
}
}
}
}
/**
* @brief 对FLASH读取数据,
* @retval 无
*/
void SPI_FLASH_BufferRead(u8* pBuffer, u32 ReadAddr, u16 NumByteToRead)
{
/* 选择FLASH: CS低电平 */
SPI_FLASH_CS_LOW();
/* 发送 读 指令 */
SPI_FLASH_SendByte(W25X_ReadData);
/* 发送 读 地址高位 */
SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16);
/* 发送 读 地址中位 */
SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8);
/* 发送 读 地址低位 */
SPI_FLASH_SendByte(ReadAddr & 0xFF);
/* 读取数据 */
while (NumByteToRead--) /* while there is data to be read */
{
/* 读取一个字节*/
*pBuffer = SPI_FLASH_SendByte(Dummy_Byte);
/* 指向下一个字节缓冲区 */
pBuffer++;
}
/* 停止信号 FLASH: CS 高电平 */
SPI_FLASH_CS_HIGH();
}
读写FLASH就完成了,要是看着乱,那就仔细梳理吧,鼓捣累了,我就休息休息了,IIC明天继续。--------------------------------------
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------2020.06.08 23:23--------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------以下内容编辑于20.06.09 18:40----------------------------------------------------------------------
IIC简介:
1.总线配置:
(1)SCL(serial clock):时钟线,传输CLK信号,一般是I2C主设备向从设备提供时钟的通道。
(2)SDA(serial data):数据线,通信数据(命令、地址、数据)都通过SDA线传输。
2.通信特征:串行、同步、电平、低速率
(1)串行通信。所有的数据以bit为单位在SDA线上串行传输(但每次传输8bit)。
(2)同步通信。即通信双方工作在同一个时钟。A方通过一根CLK信号线传输A的时钟给B,B工作在A传输的时钟下。
(3)非差分。因为I2C通信速率不高,而且通信双方距离很近,所以使用电平信号通信。
(4)低速率。I2C一般用在同一个板子上的2个IC之间,传输的数据量不大,因此本身通信速率很低(一般几百KHz,不同的I2C芯片的通信速率可能不同,具体在编程的时候要看设备允许的I2C通信最高速率)。
特征:
(1)主设备+从设备
分主设备和从设备。通信由主设备发起和主导,从设备只是按照I2C时序协议被动的接受主设备的通信。
谁是主从设备,由通信双方来决定(I2C协议并无规定)。
(2)可以多个设备挂在一条总线上(从设备地址)
主设备负责调度总线,决定某一时间和哪个从设备通信。
同一时刻,只能有一个从设备和主设备通信,其他从设备处于“冬眠”状态。
每个I2C从设备在通信中都有一个I2C从设备地址,共7个bit,广播地址全0。
它是从设备本身固有的属性。通信时主设备需要知道从设备的地址,然后在通信中通过地址来甄别是不是自己要找的那个从设备。这个地址是一个电路板上唯一的,不是全球唯一的。
系统中可能有多个同种芯片,为此addr分为固定部分和可编程部份,细节视芯片而定,看datasheet。
理论上7位有128位地址,然而除去保留几个保留地址如广播地址0x00等,数量少于128个,且标准协议里预见了地址的局限性,扩充了10位地址的概念。
电平示意图:
3、数据在总线上的传输协议

主设备首先会发送7bit位的slave device地址,和1bit位的rean或者write命令。(这里的读写,讲的是主机对总线的操作。比如写,主机写数据到总线,那么从机是读的;比如读,主机读总线的数据,那么从机是写的。)
(1)write命令
如果为write命令,则主设备释放总线(If the I2C-bus is free, both SDA and SCL lines should be both at High level),即SDA为高位;然后从设备拉低SDA,表示ACK主设备;然后主设备再发送8bit数据,从设备再ACK(A),通信结束(P)。
写寄存器的标准流程:
1. Master发起START
2. Master发送I2C addr(7bit)和w操作0(1bit),等待ACK
3. Slave发送ACK
4. Master发送reg addr(8bit),等待ACK
5. Slave发送ACK
6. Master发送data(8bit),即要写入寄存器中的数据,等待ACK
7. Slave发送ACK
8. 第6步和第7步可以重复多次,即顺序写多个寄存器
9. Master发起STOP
(2)read命令
如果为read命令,则从设备先拉低SDA表示ACK主设备,然后再发送8bit数据。主设备拉低SDA表示ACK从设备(我已经读取8bit的数据了),之后结束。
读寄存器的标准流程
1、Master发送I2Caddr(7bit)和 W操作1(1bit),等待ACK
2. Slave发送ACK
3. Master发送reg addr(8bit),等待ACK
4. Slave发送ACK
5. Master发起START
6. Master发送I2C addr(7bit)和 R操作1(1bit),等待ACK
7. Slave发送ACK
8. Slave发送data(8bit),即寄存器里的值
9. Master发送ACK
10. 第8步和第9步可以重复多次,即顺序读多个寄存器
应用实例:
IIC常运用在读写EEPROM、与传感器进行通信。就拿读写EEPROM为例,整点源码放在这。据说硬件IIC是有问题的,一般都用电平的高低来模拟硬件IIC,达到通信的效果。
/*
*********************************************************************************************************
* 函 数 名: i2c_Start
* 功能说明: CPU发起I2C总线启动信号
* 形 参:无
* 返 回 值: 无
*********************************************************************************************************
*/
void i2c_Start(void)
{
/* 当SCL高电平时,SDA出现一个下跳沿表示I2C总线启动信号 */
EEPROM_I2C_SDA_1();
EEPROM_I2C_SCL_1();
i2c_Delay();
EEPROM_I2C_SDA_0();
i2c_Delay();
EEPROM_I2C_SCL_0();
i2c_Delay();
}
/*
*********************************************************************************************************
* 函 数 名: i2c_Start
* 功能说明: CPU发起I2C总线停止信号
* 形 参:无
* 返 回 值: 无
*********************************************************************************************************
*/
void i2c_Stop(void)
{
/* 当SCL高电平时,SDA出现一个上跳沿表示I2C总线停止信号 */
EEPROM_I2C_SDA_0();
EEPROM_I2C_SCL_1();
i2c_Delay();
EEPROM_I2C_SDA_1();
}
/*
*********************************************************************************************************
* 函 数 名: ee_CheckOk
* 功能说明: 判断串行EERPOM是否正常
* 形 参:无
* 返 回 值: 1 表示正常, 0 表示不正常
*********************************************************************************************************
*/
uint8_t ee_CheckOk(void)
{
if (i2c_CheckDevice(EEPROM_DEV_ADDR) == 0)
{
return 1;
}
else
{
/* 失败后,切记发送I2C总线停止信号 */
i2c_Stop();
return 0;
}
}
上电先检查一下EEPROM工作是否正常,确认无误后再进行通信。
/*
*********************************************************************************************************
* 函 数 名: i2c_CheckDevice
* 功能说明: 检测I2C总线设备,CPU向发送设备地址,然后读取设备应答来判断该设备是否存在
* 形 参:_Address:设备的I2C总线地址
* 返 回 值: 返回值 0 表示正确, 返回1表示未探测到
*********************************************************************************************************
*/
uint8_t i2c_CheckDevice(uint8_t _Address)
{
uint8_t ucAck;
i2c_CfgGpio(); /* 配置GPIO */
i2c_Start(); /* 发送启动信号 */
/* 发送设备地址+读写控制bit(0 = w, 1 = r) bit7 先传 */
i2c_SendByte(_Address | EEPROM_I2C_WR);
ucAck = i2c_WaitAck(); /* 检测设备的ACK应答 */
i2c_Stop(); /* 发送停止信号 */
return ucAck;
}
/*
*********************************************************************************************************
* 函 数 名: i2c_CfgGpio
* 功能说明: 配置I2C总线的GPIO,采用模拟IO的方式实现
* 形 参:无
* 返 回 值: 无
*********************************************************************************************************
*/
static void i2c_CfgGpio(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(EEPROM_RCC_I2C_PORT, ENABLE); /* 打开GPIO时钟 */
GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SCL_PIN | EEPROM_I2C_SDA_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; /* 开漏输出 */
GPIO_Init(EEPROM_GPIO_PORT_I2C, &GPIO_InitStructure);
/* 给一个停止信号, 复位I2C总线上的所有设备到待机模式 */
i2c_Stop();
}
/*
*********************************************************************************************************
* 函 数 名: i2c_SendByte
* 功能说明: CPU向I2C总线设备发送8bit数据
* 形 参:_ucByte : 等待发送的字节
* 返 回 值: 无
*********************************************************************************************************
*/
void i2c_SendByte(uint8_t _ucByte)
{
uint8_t i;
/* 先发送字节的高位bit7 */
for (i = 0; i < 8; i++)
{
if (_ucByte & 0x80)
{
EEPROM_I2C_SDA_1();
}
else
{
EEPROM_I2C_SDA_0();
}
i2c_Delay();
EEPROM_I2C_SCL_1();
i2c_Delay();
EEPROM_I2C_SCL_0();
if (i == 7)
{
EEPROM_I2C_SDA_1(); // 释放总线
}
_ucByte <<= 1; /* 左移一个bit */
i2c_Delay();
}
}
/*
*********************************************************************************************************
* 函 数 名: ee_WriteBytes
* 功能说明: 向串行EEPROM指定地址写入若干数据,采用页写操作提高写入效率
* 形 参:_usAddress : 起始地址
* _usSize : 数据长度,单位为字节
* _pWriteBuf : 存放读到的数据的缓冲区指针
* 返 回 值: 0 表示失败,1表示成功
*********************************************************************************************************
*/
uint8_t ee_WriteBytes(uint8_t *_pWriteBuf, uint16_t _usAddress, uint16_t _usSize)
{
uint16_t i,m;
uint16_t usAddr;
/*
写串行EEPROM不像读操作可以连续读取很多字节,每次写操作只能在同一个page。
对于24xx02,page size = 8
简单的处理方法为:按字节写操作模式,没写1个字节,都发送地址
为了提高连续写的效率: 本函数采用page wirte操作。
*/
usAddr = _usAddress;
for (i = 0; i < _usSize; i++)
{
/* 当发送第1个字节或是页面首地址时,需要重新发起启动信号和地址 */
if ((i == 0) || (usAddr & (EEPROM_PAGE_SIZE - 1)) == 0)
{
/* 第0步:发停止信号,启动内部写操作 */
i2c_Stop();
/* 通过检查器件应答的方式,判断内部写操作是否完成, 一般小于 10ms
CLK频率为200KHz时,查询次数为30次左右
*/
for (m = 0; m < 1000; m++)
{
/* 第1步:发起I2C总线启动信号 */
i2c_Start();
/* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
i2c_SendByte(EEPROM_DEV_ADDR | EEPROM_I2C_WR); /* 此处是写指令 */
/* 第3步:发送一个时钟,判断器件是否正确应答 */
if (i2c_WaitAck() == 0)
{
break;
}
}
if (m == 1000)
{
goto cmd_fail; /* EEPROM器件写超时 */
}
/* 第4步:发送字节地址,24C02只有256字节,因此1个字节就够了,如果是24C04以上,那么此处需要连发多个地址 */
i2c_SendByte((uint8_t)usAddr);
/* 第5步:等待ACK */
if (i2c_WaitAck() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
}
/* 第6步:开始写入数据 */
i2c_SendByte(_pWriteBuf[i]);
/* 第7步:发送ACK */
if (i2c_WaitAck() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
usAddr++; /* 地址增1 */
}
/* 命令执行成功,发送I2C总线停止信号 */
i2c_Stop();
return 1;
cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
/* 发送I2C总线停止信号 */
i2c_Stop();
return 0;
}
/*
*********************************************************************************************************
* 函 数 名: i2c_ReadByte
* 功能说明: CPU从I2C总线设备读取8bit数据
* 形 参:无
* 返 回 值: 读到的数据
*********************************************************************************************************
*/
uint8_t i2c_ReadByte(void)
{
uint8_t i;
uint8_t value;
/* 读到第1个bit为数据的bit7 */
value = 0;
for (i = 0; i < 8; i++)
{
value <<= 1;
EEPROM_I2C_SCL_1();
i2c_Delay();
if (EEPROM_I2C_SDA_READ())
{
value++;
}
EEPROM_I2C_SCL_0();
i2c_Delay();
}
return value;
}
/*
*********************************************************************************************************
* 函 数 名: ee_ReadBytes
* 功能说明: 从串行EEPROM指定地址处开始读取若干数据
* 形 参:_usAddress : 起始地址
* _usSize : 数据长度,单位为字节
* _pReadBuf : 存放读到的数据的缓冲区指针
* 返 回 值: 0 表示失败,1表示成功
*********************************************************************************************************
*/
uint8_t ee_ReadBytes(uint8_t *_pReadBuf, uint16_t _usAddress, uint16_t _usSize)
{
uint16_t i;
/* 采用串行EEPROM随即读取指令序列,连续读取若干字节 */
/* 第1步:发起I2C总线启动信号 */
i2c_Start();
/* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
i2c_SendByte(EEPROM_DEV_ADDR | EEPROM_I2C_WR); /* 此处是写指令 */
/* 第3步:等待ACK */
if (i2c_WaitAck() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 第4步:发送字节地址,24C02只有256字节,因此1个字节就够了,如果是24C04以上,那么此处需要连发多个地址 */
i2c_SendByte((uint8_t)_usAddress);
/* 第5步:等待ACK */
if (i2c_WaitAck() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 第6步:重新启动I2C总线。前面的代码的目的向EEPROM传送地址,下面开始读取数据 */
i2c_Start();
/* 第7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
i2c_SendByte(EEPROM_DEV_ADDR | EEPROM_I2C_RD); /* 此处是读指令 */
/* 第8步:发送ACK */
if (i2c_WaitAck() != 0)
{
goto cmd_fail; /* EEPROM器件无应答 */
}
/* 第9步:循环读取数据 */
for (i = 0; i < _usSize; i++)
{
_pReadBuf[i] = i2c_ReadByte(); /* 读1个字节 */
/* 每读完1个字节后,需要发送Ack, 最后一个字节不需要Ack,发Nack */
if (i != _usSize - 1)
{
i2c_Ack(); /* 中间字节读完后,CPU产生ACK信号(驱动SDA = 0) */
}
else
{
i2c_NAck(); /* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */
}
}
/* 发送I2C总线停止信号 */
i2c_Stop();
return 1; /* 执行成功 */
cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
/* 发送I2C总线停止信号 */
i2c_Stop();
return 0;
}
这样就是一个完整的读写EEPROM的过程了,看懂了整个过程,拿来用就行了,本篇文章到此结束。敬礼!
1万+



被折叠的 条评论
为什么被折叠?



