文章目录
1 什么是IIC? IIC传输有什么优点? 为什么?
2 叙述IIC传输协议和IIC传输的全过程。
3 stm32IIC结构框图
4 如何编程实现IIC?
一、IIC基础知识
1.1 IIC简介
IIC(Inter-Integrated Circuit)集成电路总线,其实是IICBus简称,是一种总线结构。
它是Philips公式推出的一种基于两线的芯片间串行通信总线,使用多主从架构。
I2C总线有多种用途,包括CRC码的生成和校验、MBus(SystemManagement Bus)、PMBus(Power Management Bus )。
IIC特点:
- 二线传输
IIC总线是各种总线中使用信号线最少。一根线是同步时钟线SCL,另一根线是传输数据线SDA。 - 无中心主机
IIC总线上可以连接多个MCU主机。每个MCU都可以选择作为主模式( Master)或从模式( Slave)工作。 - 软件寻址
挂在IIC总线上的设备都可以通过编程设置地址。 - 应答式数据传输过程
设备收到数据后要应答。提高数据传输的可靠性。 - 设备可带电接入或撤出
因为IIC采用开漏输出(1.3物理层特点介绍为什么使用开漏输出),设备带电接入撤出不会影响IIC总线上的数据传输。对硬件的修改很方便。
使用I2C总线设计计算机系统十分方便灵活,体积也小,因而在各类实际应用中得到广泛应用。
串行通信的几个标准对比如下表:
1.2 IIC总线系统结构
- IIC总线上的设备必须接共同的GND。
- I2C总线只使用两条总线线路,SDA、 SCL。
各器件的SDA及SCL都是线与关系:连到总线上的任一器件输出的低电平,都将使总线的信号变低。 - 当总线空闲时,两根线均为高电平。
由IIC总线系统结构图可知,I2C总线通过上拉电阻接正电源。 - IIC设备最好设置成开漏输出,IIC设备空闲时,输出高阻态。
点击这里了解GPIO开漏输出
假设挂在IIC上的设备A设置为推挽输出,设备A空闲时,输出低电平(输出接GND)。于是总线因此也被迫接GND,电平就会被拉低,变成低电平。这将会影响到IIC的数据传输。
若设备A设置为开漏输出,空闲时输出高阻态,高阻态相当于断路,这将不会影响IIC的数据传送。 - 仲裁方式决定占用总线设备
多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。 - 微控制器可以选择作为主模式( Master) 或 从模式( Slave) 工作。
主模式:就是主CPU作为主机,向从机(挂载器件)发送接收数据。
从模式:就是主CPU作为从机,接收和发送主机(挂载器件)数据。
任一时刻IIC总线上只允许一个微控制器工作在主模式下,作为主控器;而另一个微控制器必须工作在从模式下,作为被控器。 - 三种传输模式
具有三种传输模式:标准模式传输速率为100kbit/s ,快速模式为400kbit/s ,高速模式下可达3. 4Mbit/s, 但目前大多I2C设备尚不支持高速模式。
1.4 IIC通信协议
通俗易懂的IIC讲解:B站硬件学姐IIC讲解
-
空闲状态
I2C总线通过上拉电阻接正电源。当总线空闲时,两根线均为高电平。 -
起始信号
SCL线为高电平期间,SDA线由高电平向低电平的变化表示起始信号。起始信号产生后,总线处于被占用的状态。
-
数据传送
■ 一帧共9bit:8bit主机数据 + 1bit从机应答
■ 数据传送时先传送最高位
■ 数据的有效性
SCL为高电平的时,SDA表示的数据有效,即此时的SDA为高电平时表示数据“1”,为低电平时表示数据“0”。
当SCL为低电平时,SDA的数据无效,一般在这个时候SDA进行电平切换,为下一次表示数据做好准备。
■ 传输的第一个数据 为8bit的寻址字节,寻址字节后面发送的是传输的数据。
寻址字节:D7~D1位组成从机的地址。D0位是读写位,为“0”时表示主机向从机写数据,为“1”时表示主机由从机读数据。被寻的从机会响应一个有效应答信号。
最后是主机与从机之间传输数据(8bit),数据接收方接到数据会响应一个有效应答信号ACK 。
■ 应答信号ACK
有效应答信号ACK 为 低电平0 。
-
终止信号。
在数据传输完成后,总是由主控器发出停止信号。但是,若主机希望继续占用总线进行新的数据传送,则可以不产生终止信号,马上再次发出起始信号对另一从机进行寻址。
SCL线为高电平期间,SDA线由低电平向高电平的变化表示终止信号。终止信号产生后,总线处于空闲的状态。
更加具体的协议图解如下:
1.5 常见的数据传输方式
灰色部分是主机向从机传送的数据;白色部分是从机向主机传送的数据。
- 主机传送数据给从机
主机向从机发送数据,数据传送方向在整个传送过程中不变。
- 主机直接读取从机数据。
主机在发送第一个表示从机地址及读取操作的字节后,立即读取从机数据。
- 主机指定位置读取从机数据。
在这个过程中,当需要改变传送方向时,起始信号和从机地址都被重复产生一次,但两次读/写方向位正好反相。
主机指定位置读取从机数据的过程:如下例子
简述通过I2C接口读取设备X的寄存器Y的值的过程
发送起始信号——发送设备X地址+读写位0——读取ACK——发送寄存器地址Y——读取ACK——重复起始信号——发送设备X地址+读写位1——读取ACK——读取数据——发送NACK——发送停止信号
1.6 笔试题:
简述通过I2C接口读取设备X的寄存器Y的值的过程
发送起始信号——发送设备X地址+读写位0——读取ACK——发送寄存器地址Y——读取ACK——重复起始信号——发送设备X地址+读写位1——读取ACK——读取数据——发送NACK——发送停止信号
二、STM32的IIC
2.1 stm32的IIC实现方式
- 软件模拟协议:
使用CPU直接控制通讯引脚的电平,产生出符合通讯协议标准的逻辑。 - 硬件实现协议:
由STM32的IIC片上外设专门负责实现I2C通讯协议,只要配置好该外设,它就会自动根据协议要求产生通讯信号,收发数据并缓存起来,CPU只要检测该外设的状态和访问数据寄存器,就能完成数据收发。这种由硬件外设处理IIC协议的方式减轻了CPU的工作,且使软件设计更加简单。
2.2 stm32的IIC框图(了解)
-
通讯引脚
SDA、SCL连接到相应的引脚
SMBA是用来做实现SMBA协议,很少使用。 -
时钟控制逻辑
SCL线的时钟信号,由I2C接口根据时钟控制寄存器(CCR)控制,控制的参数主要为时钟频率。
■ 可选择I2C通讯的“标准/快速”模式,这两个模式分别I2C对应100/400Kbit/s的通讯速率。
■ 在快速模式下可选择SCL时钟的占空比,可选Tlow/Thigh=2 或 Tlow/Thigh=16/9模式。
■ CCR寄存器中12位的配置因子CCR,它与12C外设的输入时钟源共同作用,产生SCL时钟。STM32的I2C外设输入时钟源为PCLK1。
-
数据控制逻辑
I2C的SDA信号主要连接到数据移位寄存器上,数据移位寄存器的数据来源是数据寄存器(DR)、目标时是目标地址寄存器(OAR) PEC寄存器以及SDA数据线。
当向外发送数据的时候,数据移位寄存器以“数据寄存器”为数据源,把数据一位一位地通过SDA信号线发送出去;
当从外部接收数据的时候,数据移位寄存器把SDA信号线采样到的数据一位一位地存储到“数据寄存器”中。 -
整体控制逻辑
整体控制逻辑负责协调整个I2C外设,控制逻辑的工作模式根据我们配置的“控制寄存器(CR1/CR2)”的参数而改变。
在外设工作时,控制逻辑会根据外设的工作状态修改“状态寄存器(SR1和SR2)”,只要读取这些寄存器相关的寄存器位,就可以了解I2C的工作状态。常使用到SR2中的BUSY标志位来判断是由正在传输数据。
2.3 stm32的硬件实现IIC通讯过程。(了解)
使用12C外设通讯时,在通讯的不同阶段它会对“状态寄存器(SR1及SR2)”的不同数据位写入参数,通过读取这些寄存器标志来了解通讯状态。
主发生器
-
控制产生起始信号(S),当发生起始信号START后,它产生事件“EV5”,并会对SR1寄存器的“SB”位置1,表示起始信号已经发送;
-
发送设备地址(从地址通过内部移位寄存器被送到SDA线上)并等待应答信号,若有从机应答,则产生事件“EV6”及“EV8”,这时SR1寄存器的“ADDR”位及“TXE”位被置1。ADDR为1表示地址已经发送,TXE为1表示数据寄存器为空;
-
往I2C的“数据寄存器DR”写入要发送的数据,这时TXE位会被重置0,表示数据寄存器非空,I2C外设通过SDA信号线一位位把数据发送出去后, 又会产生“EV8”事件,即TXE位被置1,重复这个过程,可以发送多个字节数据;
-
发送数据完成后,控制I2C设备产生一一个停止信号 ( P ),这个时候会产生EV2 事件,SR1的TXE位及BTF位都被置1,表示通讯结束。
主接收器
- 起始信号(S)是由主机端产生的,控制发生起始信号后,它产生事件“EV5”,并会对SR1寄存器的“SB”位置1,表示起始信号已经发送;
- 发送设备地址并等待应答信号,若有从机应答,则产生事件“EV6”这时SR1寄存器的“ADDR”位被置1,表示地址已经发送。
- 从机端接收到地址后, 开始向主机端发送数据。当主机接收到这些数据后,会产生“EV7"事件,SR1寄存器的RXNE被置1,表示接收数据寄存器非空,读取该寄存器后,可对数据寄存器清空,以便接收下一次数据。此时可以控制I2C发送应答信号(ACK)或非应答信号(NACK),若应答,则重复以上步骤接收数据,若非应答,则停止传输;
- 发送非应答信号后,产生停止信号( P ),结束传输。
2.4 IIC库函数
IIC初始化结构体
typedef struct
{
uint32_t I2C_ClockSpeed; /*设置SCL时钟频率,此值要低于40 000 */
uint16_t I2C_Mode; /*!< 指定工作模式,可选IIC模式及SMBUS模式 */
uint16_t I2C_DutyCycle; /*!< 指定时钟占空比,可选low/high = 2:1 及 16:9模式 */
uint16_t I2C_OwnAddress1; /*!< 指定自身的IIC设备地址,参数可以是 7-bit 或者 10-bit */
uint16_t I2C_Ack; /*!< 使能或关闭响应(一般都有使能) */
uint16_t I2C_AcknowledgedAddress; /*!<指定地址的长度,可为 7-bit 及 10-bit */
}I2C_InitTypeDef;
- I2C_ClockSpeed
设置I2C的传输速率,在调用初始化函数时,函数会根据我们输入的数值经过运算后把时钟因子写入到I2C的时钟控制寄存器CCR。而我们写入的这个参数值不得高于400KHz。
库函数会根据I2C_ClockSpeed的值自动配置为快速模式或者是标准模式。 - I2C_Mode
选择I2C的使用方式,有I2C模式(I2C_Mode_l2C )和SMBus主、从模式(I2C_Mode_SMBusHost、I2C_Mode_SMBusDevice )。
I2C不需要在此处区分主从模式,直接设置I2C_Mode_I2C即可。 - I2C_DutyCycle
设置I2C的SCL线时钟的占空比。该配置有两个选择,分别为低电平时间比高电平时间为2: 1 ( I2C_DutyCycle_2)和16: 9(12C_DutyCycle_16_9)。
其实这两个模式的比例差别并不大,一般要求都不会如此严格,这里随便选就可以了。 - I2C_OwnAddress1
配置STM32的I2C设备自己的地址,每个连接到I2C总线上的设备都要有一个自己的地址,作为主机也不例外。地址可设置为7位或10位(受下面I2C_AcknowledgeAddress成员决定),只要该地址是I2C总线上唯一的即可。
STM32的I2C外设可同时使用两个地址,即同时对两个地址作出响应,这个结构成员I2C_OwnAddress1配 置的是默认的、OAR1寄存器存储的地址,若需要设置第二个地址寄存器OAR2,可使用I2C_OwnAddress2Config函 数来配置,OAR2不支持10位地址。 - I2C_Ack
配置2C应答是否使能,设置为使能则可以发送响应信号。一般配置为允许应答(I2C Ack/Enable), 这是绝大多数遵循I2C标准的设备的通讯要求,改为禁止应答(12C_Ack_Disable)往往 会导致通讯错误。 - I2C_AcknowledgeAddress
选择I2C的寻址模式是7位还是10位地址。这需要根据实际连接到I2C总线上设备的地址进行选择,这个成员的配置也影响到l2C_OwnAddress1成员, 只有这里设置成10位模式时,l2C_OwnAddress1才支持10位地址。
产生起始、停止信号函数
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState)
{
/* Check the parameters */
assert_param(IS_I2C_ALL_PERIPH(I2Cx));
assert_param(IS_FUNCTIONAL_STATE(NewState));
if (NewState != DISABLE)
{
/* Generate a START condition */
I2Cx->CR1 |= CR1_START_Set;
}
else
{
/* Disable the START condition generation */
I2Cx->CR1 &= CR1_START_Reset;
}
}
- I2Cx
通这个参数来选择I2C外设。可以选择 I2C1 或 I2C2 。 - NewState
通过此来选择是否产生起始位。可以选择 ENABLE 或 DISABLE。
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState)
{
/* Check the parameters */
assert_param(IS_I2C_ALL_PERIPH(I2Cx));
assert_param(IS_FUNCTIONAL_STATE(NewState));
if (NewState != DISABLE)
{
/* Generate a STOP condition */
I2Cx->CR1 |= CR1_STOP_Set;
}
else
{
/* Disable the STOP condition generation */
I2Cx->CR1 &= CR1_STOP_Reset;
}
}
同 I2C_GenerateSTART();
读取标志位函数
FlagStatus I2C_GetFlagStatus(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG)
{
//...
}
- FlagStatus
返回 SET 或者 RESET - I2Cx
通这个参数来选择I2C外设。可以选择 I2C1 或 I2C2 。 - I2C_FLAG
标志位,库函数已经宏定义好了这些标志位,如下。
- This parameter can be one of the following values:
- @arg I2C_FLAG_DUALF: Dual flag (Slave mode)
- @arg I2C_FLAG_SMBHOST: SMBus host header (Slave mode)
- @arg I2C_FLAG_SMBDEFAULT: SMBus default header (Slave mode)
- @arg I2C_FLAG_GENCALL: General call header flag (Slave mode)
- @arg I2C_FLAG_TRA: Transmitter/Receiver flag
- @arg I2C_FLAG_BUSY: Bus busy flag
- @arg I2C_FLAG_MSL: Master/Slave flag
- @arg I2C_FLAG_SMBALERT: SMBus Alert flag
- @arg I2C_FLAG_TIMEOUT: Timeout or Tlow error flag
- @arg I2C_FLAG_PECERR: PEC error in reception flag
- @arg I2C_FLAG_OVR: Overrun/Underrun flag (Slave mode)
- @arg I2C_FLAG_AF: Acknowledge failure flag
- @arg I2C_FLAG_ARLO: Arbitration lost flag (Master mode)
- @arg I2C_FLAG_BERR: Bus error flag
- @arg I2C_FLAG_TXE: Data register empty flag (Transmitter)
- @arg I2C_FLAG_RXNE: Data register not empty (Receiver) flag
- @arg I2C_FLAG_STOPF: Stop detection flag (Slave mode)
- @arg I2C_FLAG_ADD10: 10-bit header sent flag (Master mode)
- @arg I2C_FLAG_BTF: Byte transfer finished flag
- @arg I2C_FLAG_ADDR: Address sent flag (Master mode) "ADSL"
- Address matched flag (Slave mode)"ENDA"
- @arg I2C_FLAG_SB: Start bit flag (Master mode)
发送地址函数
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction)
{
/* Check the parameters */
assert_param(IS_I2C_ALL_PERIPH(I2Cx));
assert_param(IS_I2C_DIRECTION(I2C_Direction));
/* Test on the direction to set/reset the read/write bit */
if (I2C_Direction != I2C_Direction_Transmitter)
{
/* Set the address bit0 for read */
Address |= OAR1_ADD0_Set;
}
else
{
/* Reset the address bit0 for write */
Address &= OAR1_ADD0_Reset;
}
/* Send the address */
I2Cx->DR = Address;
}
- I2Cx
通这个参数来选择I2C外设。可以选择 I2C1 或 I2C2 。 - Address
指定的从设备地址。
根据 I2C_Direction 对 Address 与上 0xfffe 得到写地址 或者 或上 0x0001 得到读地址。故8-bit的 Address 可以由7-bit外设地址加上任意的最后一位构成。 - I2C_Direction
方向:发送还是接收。参数可以选择发送模式 I2C_Direction_Transmitter 或者 接收模式 I2C_Direction_Receiver 。
发送数据函数
/**
* @brief Sends a data byte through the I2Cx peripheral.
* @param I2Cx: where x can be 1 or 2 to select the I2C peripheral.
* @param Data: Byte to be transmitted..
* @retval None
*/
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data)
{
/* Check the parameters */
assert_param(IS_I2C_ALL_PERIPH(I2Cx));
/* Write in the DR register the data to be sent */
I2Cx->DR = Data;
}
- I2Cx
通这个参数来选择I2C外设。可以选择 I2C1 或 I2C2 。 - Data
要发送的 8-bit 数据
接收数据函数
/**
* @brief Returns the most recent received data by the I2Cx peripheral.
* @param I2Cx: where x can be 1 or 2 to select the I2C peripheral.
* @retval The value of the received data.
*/
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx)
{
/* Check the parameters */
assert_param(IS_I2C_ALL_PERIPH(I2Cx));
/* Return the data in the DR register */
return (uint8_t)I2Cx->DR;
}
读取接收到的数据。
- I2Cx
通这个参数来选择I2C外设。可以选择 I2C1 或 I2C2 。
应答信号配置函数
/**
* @brief Enables or disables the specified I2C acknowledge feature.
* @param I2Cx: where x can be 1 or 2 to select the I2C peripheral.
* @param NewState: new state of the I2C Acknowledgement.
* This parameter can be: ENABLE or DISABLE.
* @retval None.
*/
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState)
{
/* Check the parameters */
assert_param(IS_I2C_ALL_PERIPH(I2Cx));
assert_param(IS_FUNCTIONAL_STATE(NewState));
if (NewState != DISABLE)
{
/* Enable the acknowledgement */
I2Cx->CR1 |= CR1_ACK_Set;
}
else
{
/* Disable the acknowledgement */
I2Cx->CR1 &= CR1_ACK_Reset;
}
}
- I2Cx
通这个参数来选择I2C外设。可以选择 I2C1 或 I2C2 。 - NewState
通过此来选择是否产生应答信号。可以选择 ENABLE 或 DISABLE。在通讯计数是,设置为 DISABLE 结束通讯。
三、软件模拟I2C,读写EEPROM
为啥使用 EEPROM 做IIC实验呢?便宜且使用IIC协议通讯。
EEPROM-AT24C02具体请看 AT24C02简介
开发板上的EEPROM连接电路图:
3.1 软件实现简单读写EEPROM
具体思路:
- 首先初始化IIC
把连接EEPROM的SCL、SDA引脚的GPIO设置为推挽输出。 - 然后通过字节写入的方法写入1 byte的数据到eeprom。
我们要模拟时序先后产生起始信号、写入设备信号、写入数据内存地址信号、写入数据信号、结束信号来实现写数据。 - 最后通过指定地址的方法再读取出写入的数据。
这里我们要模拟时序先后产生起始信号、写入设备信号、写入数据内存地址信号、起始信号、读取设备信号、接收内存数据、产生结束信号。
测试代码:
iic.h
#ifndef IIC_H
#define IIC_H
#include "stm32f10x.h"
#include "Tool.h"//这个文件实现了位绑定的功能
#include "bsp_systick.h"//这个文件实现systick延时的功能
/****** IIC_SCL时钟端口、引脚定义 ******/
#define IIC_SCL_PORT GPIOB
#define IIC_SCL_PIN GPIO_Pin_6
#define IIC_SCL_PORT_RCC RCC_APB2Periph_GPIOB
/****** IIC_SDA时钟端口、引脚定义 ******/
#define IIC_SDA_PORT GPIOB
#define IIC_SDA_PIN GPIO_Pin_7
#define IIC_SDA_PORT_RCC RCC_APB2Periph_GPIOB
/************* IO操作函数 *************/
#define IIC_SCL PBout(6) //SCL
#define IIC_SDA PBout(7) //SDA
#define READ_SDA PBin(7) //输入SDA
/*********** IIC所有操作函数 ***********/
void IIC_Init(void); //初始化IIC的IO口
void SDA_OUT(void); //配置IIC的SDA为输出模式
void SDA_IN(void); //配置IIC的SDA为输入模式
#endif
iic.c
#include "iic.h"
/*******************************************************************
*函数: IIC_Init
*功能: IIC初始化,配置SCL、SDA引脚为推挽输出
*输入: void
*输出: None
*特殊说明:
*******************************************************************/
void IIC_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(IIC_SCL_PORT_RCC|IIC_SDA_PORT_RCC,ENABLE);
GPIO_InitStructure.GPIO_Pin = IIC_SCL_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(IIC_SCL_PORT,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = IIC_SDA_PIN;
GPIO_Init(IIC_SCL_PORT,&GPIO_InitStructure);
IIC_SCL = 1;
IIC_SDA = 1;
}
/*******************************************************************
*函数: SDA_OUT
*功能: 初始化输出IO。配置SDA引脚为推挽输出
*输入: void
*输出: None
*特殊说明:不配置成开漏输出,是为了方便操作
*******************************************************************/
void SDA_OUT(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = IIC_SDA_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(IIC_SDA_PORT,&GPIO_InitStructure);
}
/*******************************************************************
*函数: SDA_IN
*功能: 初始化输入IO。配置SDA引脚为上拉输入
*输入: void
*输出: None
*特殊说明:此为软件模拟IIC,使用硬件是要配置成开漏输出
*******************************************************************/
void SDA_IN(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = IIC_SDA_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(IIC_SDA_PORT,&GPIO_InitStructure);
}
}
main.c
#include "stm32f10x.h"
#include "bsp_systick.h"
#include "usart.h"
#include "led.h"
#include "key.h"
#include "iic.h"
int main(void)
{
u8 txd=0;
u8 readData=0;
u8 i=0;
usart1_Init(9600);
LED_Init();
KEY_Init();
IIC_Init();
printf("开始检测数据!\r\n");
SDA_IN();//SDA设置为输入
if(READ_SDA == 1)//检测一下电路是否接上拉电阻
printf("初始SDA为高电平\r\n");
/********** 起始信号 **********/
SDA_OUT();
IIC_SDA=1;
IIC_SCL=1;
SysTick_Delay_us(5);
IIC_SDA=0;//SDA拉低,作为起始信号
SysTick_Delay_us(5);
IIC_SCL=0;//拉低SCL,准备发送/接受数据
SysTick_Delay_us(5);
/********** 写入设备 **********/
SDA_OUT();
txd=0xa0;//发送的寻找设备的地址
for(i=0;i<8;i++)
{
IIC_SCL=0;//根据通讯有效数据标准,先使SCL=0,在改变数据,若初始化时SCL=0,这句可以注释掉
if((txd&0x80)>0)
IIC_SDA=1;
else
IIC_SDA=0;
txd<<=1;
SysTick_Delay_us(1);//根据时序要求,SCL低电平周期要持续2us
IIC_SCL=1;SysTick_Delay_us(2);//根据时序要求,SCL高电平周期要持续2us
IIC_SCL=0;SysTick_Delay_us(1);//根据时序要求,SCL低电平周期要持续2us
}
/********** 检查ack **********/
SDA_IN();//SDA设置为输入
IIC_SCL=1;SysTick_Delay_us(1);//上拉SCL,使数据位有效,通知eeprom现在数据有效,别更改数据
IIC_SDA=1;SysTick_Delay_us(1);//上拉SDA
for(i=0;i<250;i++)
{
if(READ_SDA == 0)
{
printf("设备响应成功\n");
IIC_SCL=0;
break;
}
}
/********** 写入内存地址 **********/
SDA_OUT();
txd =255;
for(i=0;i<8;i++)
{
IIC_SCL=0;//根据通讯有效数据标准,先使SCL=0,在改变数据
if((txd&0x80)>0)
IIC_SDA=1;
else
IIC_SDA=0;
txd<<=1;
SysTick_Delay_us(1);//根据时序要求,SCL低电平周期要持续2us
IIC_SCL=1;SysTick_Delay_us(2);//根据时序要求,SCL高电平周期要持续2us
IIC_SCL=0;SysTick_Delay_us(1);//根据时序要求,SCL低电平周期要持续2us
}
/********** 检查ack **********/
SDA_IN();//SDA设置为输入
IIC_SCL=1;SysTick_Delay_us(1);//上拉SCL,使数据位有效,通知eeprom现在数据有效,别更改数据
IIC_SDA=1;SysTick_Delay_us(1);//上拉SDA
for(i=0;i<250;i++)
{
if(READ_SDA == 0)
{
printf("写入内存地址ack返回0\n");
IIC_SCL=0;
break;
}
}
/********** 写入数据 **********/
SDA_OUT();
txd =0xee;//写入到指定地址的数据
printf("准备写入数据txd:%X\n",txd);
for(i=0;i<8;i++)
{
IIC_SCL=0;//根据通讯有效数据标准,先使SCL=0,在改变数据
if((txd&0x80)>0)
IIC_SDA=1;
else
IIC_SDA=0;
txd<<=1;
SysTick_Delay_us(1);//根据时序要求,SCL低电平周期要持续2us
IIC_SCL=1;SysTick_Delay_us(2);//根据时序要求,SCL高电平周期要持续2us
IIC_SCL=0;SysTick_Delay_us(1);//根据时序要求,SCL低电平周期要持续2us
}
/********** 检查ack **********/
SDA_IN();//SDA设置为输入
IIC_SCL=1;SysTick_Delay_us(1);//上拉SCL,使数据位有效,通知eeprom现在数据有效,别更改数据
IIC_SDA=1;SysTick_Delay_us(1);//上拉SDA
for(i=0;i<250;i++)
{
if(READ_SDA == 0)
{
printf("写入数据txd,ack返回0\n");
IIC_SCL=0;
break;
}
}
/********** 结束信号 **********/
SDA_OUT();
IIC_SDA=0;//注意一定要SDA=0在前,SCL=1在后,否则写入不能成功,不知原因
IIC_SCL=1;
SysTick_Delay_us(5);
IIC_SDA=1;
SysTick_Delay_us(5);
SysTick_Delay_ms(10);//保证数据从缓冲区写入到不丢失区
/********** 起始信号 **********/
SDA_OUT();
IIC_SDA=1;
IIC_SCL=1;
SysTick_Delay_us(5);
IIC_SDA=0;//SDA拉低,作为起始信号
SysTick_Delay_us(5);
IIC_SCL=0;//拉低SCL,准备发送/接受数据
SysTick_Delay_us(5);
/********** 写入设备 **********/
SDA_OUT();
txd=0xa0;//发送的寻找设备的地址
for(i=0;i<8;i++)
{
IIC_SCL=0;//根据通讯有效数据标准,先使SCL=0,在改变数据,若初始化时SCL=0,这句可以注释掉
if((txd&0x80)>0)
IIC_SDA=1;
else
IIC_SDA=0;
txd<<=1;
SysTick_Delay_us(1);//根据时序要求,SCL低电平周期要持续2us
IIC_SCL=1;SysTick_Delay_us(2);//根据时序要求,SCL高电平周期要持续2us
IIC_SCL=0;SysTick_Delay_us(1);//根据时序要求,SCL低电平周期要持续2us
}
/********** 检查ack **********/
SDA_IN();//SDA设置为输入
IIC_SCL=1;SysTick_Delay_us(1);//上拉SCL,使数据位有效,通知eeprom现在数据有效,别更改数据
IIC_SDA=1;SysTick_Delay_us(1);//上拉SDA
for(i=0;i<250;i++)
{
if(READ_SDA == 0)
{
printf("设备响应成功\n");
IIC_SCL=0;
break;
}
}
/********** 写入内存地址 **********/
SDA_OUT();
txd =255;
for(i=0;i<8;i++)
{
IIC_SCL=0;//根据通讯有效数据标准,先使SCL=0,在改变数据
if((txd&0x80)>0)
IIC_SDA=1;
else
IIC_SDA=0;
txd<<=1;
SysTick_Delay_us(1);//根据时序要求,SCL低电平周期要持续2us
IIC_SCL=1;SysTick_Delay_us(2);//根据时序要求,SCL高电平周期要持续2us
IIC_SCL=0;SysTick_Delay_us(1);//根据时序要求,SCL低电平周期要持续2us
}
/********** 检查ack **********/
SDA_IN();//SDA设置为输入
IIC_SCL=1;SysTick_Delay_us(1);//上拉SCL,使数据位有效,通知eeprom现在数据有效,别更改数据
IIC_SDA=1;SysTick_Delay_us(1);//上拉SDA
for(i=0;i<250;i++)
{
if(READ_SDA == 0)
{
printf("写入内存地址ack返回0\n");
IIC_SCL=0;
break;
}
}
/********** 起始信号 **********/
SDA_OUT();
IIC_SDA=1;
IIC_SCL=1;
SysTick_Delay_us(5);
IIC_SDA=0;//SDA拉低,作为起始信号
SysTick_Delay_us(5);
IIC_SCL=0;//拉低SCL,准备发送/接受数据
SysTick_Delay_us(5);
/********** 读取设备 **********/
SDA_OUT();
txd=0xa1;
for(i=0;i<8;i++)
{
IIC_SCL=0;//根据通讯有效数据标准,先使SCL=0?
if((txd&0x80)>0)
IIC_SDA=1;
else
IIC_SDA=0;
txd<<=1;
SysTick_Delay_us(1);//根据时序要求,SCL低电平周期要持续2us
IIC_SCL=1;SysTick_Delay_us(2);//根据时序要求,SCL高电平周期要持续2us
IIC_SCL=0;SysTick_Delay_us(1);//根据时序要求,SCL低电平周期要持续2us
}
/********** 检查ack **********/
SDA_IN();//SDA设置为输入
IIC_SCL=1;SysTick_Delay_us(1);//上拉SCL,使数据位有效,通知eeprom现在数据有效,别更改数据
IIC_SDA=1;SysTick_Delay_us(1);//上拉SDA
for(i=0;i<250;i++)
{
if(READ_SDA == 0)
{
printf("设备响应成功\n");
IIC_SCL=0;
break;
}
}
/********** 接受数据 **********/
SDA_IN();//SDA输入
for(i=0;i<8;i++)
{
//拉低SCL,持续一段时间,从机更改数据后,再拉高SCL
IIC_SCL=0;
SysTick_Delay_us(5);//时钟低电平大于4700ns
IIC_SCL=1;
readData<<=1;
if(READ_SDA)
readData++;//最低位置1
SysTick_Delay_us(4);//时钟高电平大于4000ns
}
printf("rec data is :%X\n",readData);
/********** 结束信号 **********/
SDA_OUT();
IIC_SDA=0;
IIC_SCL=1;
SysTick_Delay_us(5);
IIC_SDA=1;
SysTick_Delay_us(5);
}
封装好的代码
iic.h
#ifndef IIC_H
#define IIC_H
#include "stm32f10x.h"
#include "Tool.h"
#include "bsp_systick.h"
/****** IIC_SCL时钟端口、引脚定义 ******/
#define IIC_SCL_PORT GPIOB
#define IIC_SCL_PIN GPIO_Pin_6
#define IIC_SCL_PORT_RCC RCC_APB2Periph_GPIOB
/****** IIC_SDA时钟端口、引脚定义 ******/
#define IIC_SDA_PORT GPIOB
#define IIC_SDA_PIN GPIO_Pin_7
#define IIC_SDA_PORT_RCC RCC_APB2Periph_GPIOB
/************* IO操作函数 *************/
#define IIC_SCL PBout(6) //SCL
#define IIC_SDA PBout(7) //SDA
#define READ_SDA PBin(7) //输入SDA
/*********** IIC所有操作函数 ***********/
void IIC_Init(void); //初始化IIC的IO口
void SDA_OUT(void); //配置IIC的SDA为输出模式
void SDA_IN(void); //配置IIC的SDA为输入模式
void IIC_Start(void); //
void IIC_Send_Byte(u8 txd);
u8 IIC_Wait_Ack(void);
u8 IIC_Read_Byte(u8 ack);
void IIC_Stop(void);
#endif
iic.c
#include "iic.h"
/*******************************************************************
*函数: IIC_Init
*功能: IIC初始化,配置SCL、SDA引脚为推挽输出
*输入: void
*输出: None
*特殊说明:
*******************************************************************/
void IIC_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(IIC_SCL_PORT_RCC|IIC_SDA_PORT_RCC,ENABLE);
GPIO_InitStructure.GPIO_Pin = IIC_SCL_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(IIC_SCL_PORT,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = IIC_SDA_PIN;
GPIO_Init(IIC_SCL_PORT,&GPIO_InitStructure);
IIC_SCL = 1;
IIC_SDA = 1;
}
/*******************************************************************
*函数: SDA_OUT
*功能: 初始化输出IO。配置SDA引脚为推挽输出
*输入: void
*输出: None
*特殊说明:不配置成开漏输出,是为了方便操作
*******************************************************************/
void SDA_OUT(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = IIC_SDA_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(IIC_SDA_PORT,&GPIO_InitStructure);
}
/*******************************************************************
*函数: SDA_IN
*功能: 初始化输入IO。配置SDA引脚为上拉输入
*输入: void
*输出: None
*特殊说明:此为软件模拟IIC,使用硬件是要配置成开漏输出
*******************************************************************/
void SDA_IN(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = IIC_SDA_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(IIC_SDA_PORT,&GPIO_InitStructure);
}
/*******************************************************************
*函数: IIC_Start
*功能: IIC产生起始信号
*输入: void
*输出: None
*特殊说明:
*******************************************************************/
void IIC_Start(void)
{
SDA_OUT();
IIC_SDA=1;
IIC_SCL=1;
SysTick_Delay_us(4);
IIC_SDA=0;
SysTick_Delay_us(4);
IIC_SCL=0;
}
/*******************************************************************
*函数: IIC_Stop
*功能: IIC产生停止信号
*输入: void
*输出: None
*特殊说明:
*******************************************************************/
void IIC_Stop(void)
{
SDA_OUT();
IIC_SDA=0;//注意一定要SDA=0在前,SCL=1在后,否则写入不能成功,不知原因
IIC_SCL=1;
SysTick_Delay_us(5);
IIC_SDA=1;
SysTick_Delay_us(5);
}
/*******************************************************************
*函数: IIC_Wait_Ack
*功能: 等待应答信号到来。设置SCL、SDA为高电平,判断SDA是否变为0
*输入: void
*输出: 1, 表示应答失败
0,表示应答成功
*特殊说明:
*******************************************************************/
u8 IIC_Wait_Ack(void)
{
u8 waitTime = 0;
SDA_IN();//SDA设置为输入
IIC_SDA=1;SysTick_Delay_us(1);
IIC_SCL=1;SysTick_Delay_us(1);
while(READ_SDA)
{
waitTime++;
//电平在这段时间中一直为1,则说明应答失败,停止IIC通讯
if(waitTime > 250)
{
IIC_Stop();
return 1;
}
}
IIC_SCL=0;//时钟输出0
return 0;
}
/*******************************************************************
*函数: IIC_Ack
*功能: 产生ACK应答。设置SCL、SDA为低电平,判断SDA是否变为0
*输入: void
*输出: None
*特殊说明:
*******************************************************************/
void IIC_Ack(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=0;//顺序
SysTick_Delay_us(2);//顺序
IIC_SCL=1;//顺序
SysTick_Delay_us(5);
IIC_SCL=0;
}
/*******************************************************************
*函数: IIC_NAck
*功能: 产生NACK应答。设置SCL、SDA为低电平,判断SDA是否变为0
*输入: void
*输出: None
*特殊说明:
*******************************************************************/
void IIC_NAck(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=1;
SysTick_Delay_us(2);
IIC_SCL=1;
SysTick_Delay_us(2);
IIC_SCL=0;
}
/*******************************************************************
*函数: IIC_Send_Byte
*功能: 发送一字节的数据
*输入: txd : 8bit的数据
*输出: None
*特殊说明:
*******************************************************************/
void IIC_Send_Byte(u8 txd)
{
u8 i;
SDA_OUT();
IIC_SCL=0;//根据通讯有效数据标准,先使SCL=0,在改变数据
SysTick_Delay_us(5);//根据时序要求,SCL低电平周期要持续4.7us
for(i=0;i<8;i++)
{
if((txd&0x80)>0)
IIC_SDA=1;
else
IIC_SDA=0;
txd<<=1;
IIC_SCL=1;SysTick_Delay_us(5);//根据时序要求,SCL高电平周期要持续4us
IIC_SCL=0;SysTick_Delay_us(5);//根据时序要求,SCL低电平周期要持续4.7us
}
}
/*******************************************************************
*函数: IIC_Read_Byte
*功能: 读取一字节的数据
*输入: ack: ack为1,发送ACK; ack为0,发送nACK。
*输出: 接收到的数据
*特殊说明:
*******************************************************************/
u8 IIC_Read_Byte(u8 ack)
{
u8 i,receive=0;
SDA_IN();//SDA输入
for(i=0;i<8;i++)
{
IIC_SCL=0;
SysTick_Delay_us(5);//时钟低电平大于4700ns
IIC_SCL=1;
receive<<=1;//先移位,否则在接受最后一位数据时,会把第一个接受的数据移位出去
if(READ_SDA)
receive++;
SysTick_Delay_us(4);//时钟高电平大于4000ns
}
if(!ack)
IIC_NAck();//发送nACK
if(ack)
IIC_Ack();//发送ACK
return receive;
}
main.c
#include "stm32f10x.h"
#include "bsp_systick.h"
#include "usart.h"
#include "led.h"
#include "key.h"
#include "iic.h"
int main(void)
{
u8 rec=0;
usart1_Init(9600);
LED_Init();
KEY_Init();
IIC_Init();
printf("开始检测数据!\r\n");
/********* 写入数据 *********/
IIC_Start();
IIC_Send_Byte(0xa0);
if(IIC_Wait_Ack() == 0)
printf("设备响应成功\n");
IIC_Send_Byte(0xff);
if(IIC_Wait_Ack() == 0)
printf("写入地址响应成功\n");
IIC_Send_Byte(0x1c);
if(IIC_Wait_Ack() == 0)
printf("写入数据响应成功\n");
IIC_Stop();
/********* 指定内存地址读取数据 *********/
IIC_Start();
IIC_Send_Byte(0xa0);
if(IIC_Wait_Ack() == 0)
printf("设备响应成功\n");
IIC_Send_Byte(0xff);
if(IIC_Wait_Ack() == 0)
printf("写入地址响应成功\n");
IIC_Start();
IIC_Send_Byte(0xa1);
if(IIC_Wait_Ack() == 0)
printf("读取设备响应成功\n");
rec=IIC_Read_Byte(0);
printf("rec data is :%X\n",rec);
}
现象:
仿真调试,通过逻辑分析仪查看到的波形图。
最终实验现象可以通过串口助手观察到写入数据和读取数据一致。可以通过改变写入的数据值txd来验证这是否是偶然的成功。经验证不是偶然结果。
遇到的问题(应该都是关于有小电平持续时间的问题):
- 起始信号问题
起始信号的三个延时是必须的。
第一个延时使SDA=1、SCL=1持续一段时间,这个时间要大于有效的起始条件建立时间。(起始条件建立时间最小是4700ns)
第二个延时使SDA=0持续一段时间,这个时间要大于有效的起始条件保持时间。(起始条件保持时间最小是4000ns)
第二个延时使SCL=0持续一段时间,这个时间要大于时钟低电平时间。(时钟低电平时间最小是4700ns)
/********** 起始信号 **********/
SDA_OUT();
IIC_SDA=1;
IIC_SCL=1;
SysTick_Delay_us(5);
IIC_SDA=0;//SDA拉低,作为起始信号
SysTick_Delay_us(5);
IIC_SCL=0;//拉低SCL,准备发送/接受数据
SysTick_Delay_us(5);
- 写入内存地址没有接收到响应信号问题
想要接收写入内存地址的ack。首先要拉高SCL、SDA两根线。否则无法接受到Ack。原因尚不清。
注意:在接受完ack后,要把SCL置0。否则回导致后面传送数据不成功。原因尚不清。
/********** 检查ack **********/
SDA_IN();//SDA设置为输入
IIC_SCL=1;SysTick_Delay_us(1);//上拉SCL,使数据位有效,通知eeprom现在数据有效,别更改数据
IIC_SDA=1;SysTick_Delay_us(1);//上拉SDA
for(i=0;i<250;i++)
{
if(READ_SDA == 0)
{
printf("设备响应成功\n");
IIC_SCL=0;
break;
}
}
- 写入数据不成功问题
在写入数据时,回应ack为0,显然回应成功了,但是在读取数据的时候,数据没有变化。
查找bug最后发现在模拟停止信号时序时,先把IIC_SCL置1,后把IIC_SDA置0,这样导致写入失败,原因尚不清。所以在模拟结束信号时注意一定要SDA=0在前,SCL=1在后,否则写入不能成功。
/********** 结束信号 **********/
SDA_OUT();
IIC_SDA=0;//注意一定要SDA=0在前,SCL=1在后,否则写入不能成功,不知原因
IIC_SCL=1;
SysTick_Delay_us(5);
IIC_SDA=1;
SysTick_Delay_us(5);
- 写入数据不成功问题2
在写入数据后,马上读取数据,读取出来的数据是0xff
。
往EEPROM写入数据后,需要傻等5MS(资料推荐10ms,一般使用5ms!),才能真正写入成功。
- 读取数据乱码的问题
在读取时,移位出错 - 读取数据为0问题,
在SDA脚输出时要切换为输出状态;在SDA脚输入时要切换为输入状态 - EEPROM不回复问题
可能是时序问题,SCL需要按照周期性的高低高低变化,如SCL高电平时间2US,那么低电平时间也是2US,不能是一会SCL高电平1US,一会SCL低电平3US,SCL是时钟,一定要有周期性。