stm32学习笔记-常用的通信协议IIC


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特点:

  1. 二线传输
    IIC总线是各种总线中使用信号线最少。一根线是同步时钟线SCL,另一根线是传输数据线SDA。
  2. 无中心主机
    IIC总线上可以连接多个MCU主机。每个MCU都可以选择作为主模式( Master)或从模式( Slave)工作。
  3. 软件寻址
    挂在IIC总线上的设备都可以通过编程设置地址。
  4. 应答式数据传输过程
    设备收到数据后要应答。提高数据传输的可靠性。
  5. 设备可带电接入或撤出
    因为IIC采用开漏输出(1.3物理层特点介绍为什么使用开漏输出),设备带电接入撤出不会影响IIC总线上的数据传输。对硬件的修改很方便。

使用I2C总线设计计算机系统十分方便灵活,体积也小,因而在各类实际应用中得到广泛应用。

串行通信的几个标准对比如下表:
在这里插入图片描述

1.2 IIC总线系统结构

在这里插入图片描述

  1. IIC总线上的设备必须接共同的GND。
  2. I2C总线只使用两条总线线路,SDA、 SCL。
    各器件的SDA及SCL都是线与关系:连到总线上的任一器件输出的低电平,都将使总线的信号变低。
  3. 当总线空闲时,两根线均为高电平
    由IIC总线系统结构图可知,I2C总线通过上拉电阻接正电源。
  4. IIC设备最好设置成开漏输出,IIC设备空闲时,输出高阻态
    点击这里了解GPIO开漏输出
    假设挂在IIC上的设备A设置为推挽输出,设备A空闲时,输出低电平(输出接GND)。于是总线因此也被迫接GND,电平就会被拉低,变成低电平。这将会影响到IIC的数据传输。
    若设备A设置为开漏输出,空闲时输出高阻态,高阻态相当于断路,这将不会影响IIC的数据传送。
  5. 仲裁方式决定占用总线设备
    多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。
  6. 微控制器可以选择作为主模式( Master)从模式( Slave) 工作。
    主模式:就是主CPU作为主机,向从机(挂载器件)发送接收数据。
    从模式:就是主CPU作为从机,接收和发送主机(挂载器件)数据。
    任一时刻IIC总线上只允许一个微控制器工作在主模式下,作为主控器;而另一个微控制器必须工作在从模式下,作为被控器。
  7. 三种传输模式
    具有三种传输模式:标准模式传输速率为100kbit/s ,快速模式为400kbit/s ,高速模式下可达3. 4Mbit/s, 但目前大多I2C设备尚不支持高速模式。

1.4 IIC通信协议

通俗易懂的IIC讲解:B站硬件学姐IIC讲解
在这里插入图片描述

  1. 空闲状态
    I2C总线通过上拉电阻接正电源。当总线空闲时,两根线均为高电平

  2. 起始信号
    SCL线为高电平期间,SDA线由高电平向低电平的变化表示起始信号。起始信号产生后,总线处于被占用的状态。
    在这里插入图片描述

  3. 数据传送
    ■ 一帧共9bit:8bit主机数据 + 1bit从机应答
    ■ 数据传送时先传送最高位
    数据的有效性
    SCL为高电平的时,SDA表示的数据有效,即此时的SDA为高电平时表示数据“1”,为低电平时表示数据“0”。
    当SCL为低电平时,SDA的数据无效,一般在这个时候SDA进行电平切换,为下一次表示数据做好准备。
    在这里插入图片描述
    ■ 传输的第一个数据 为8bit的寻址字节,寻址字节后面发送的是传输的数据。
    寻址字节:D7~D1位组成从机的地址。D0位是读写位,为“0”时表示主机向从机写数据,为“1”时表示主机由从机读数据。被寻的从机会响应一个有效应答信号。
    在这里插入图片描述
    最后是主机与从机之间传输数据(8bit),数据接收方接到数据会响应一个有效应答信号ACK 。
    ■ 应答信号ACK
    有效应答信号ACK 为 低电平0 。
    在这里插入图片描述

  4. 终止信号
    在数据传输完成后,总是由主控器发出停止信号。但是,若主机希望继续占用总线进行新的数据传送,则可以不产生终止信号,马上再次发出起始信号对另一从机进行寻址。
    SCL线为高电平期间,SDA线由低电平向高电平的变化表示终止信号。终止信号产生后,总线处于空闲的状态。
    在这里插入图片描述

更加具体的协议图解如下:
在这里插入图片描述

1.5 常见的数据传输方式

灰色部分是主机向从机传送的数据;白色部分是从机向主机传送的数据。

  1. 主机传送数据给从机
    主机向从机发送数据,数据传送方向在整个传送过程中不变。
    在这里插入图片描述
  2. 主机直接读取从机数据
    主机在发送第一个表示从机地址及读取操作的字节后,立即读取从机数据。
    在这里插入图片描述
  3. 主机指定位置读取从机数据
    在这个过程中,当需要改变传送方向时,起始信号和从机地址都被重复产生一次,但两次读/写方向位正好反相。
    在这里插入图片描述

主机指定位置读取从机数据的过程:如下例子
简述通过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框图(了解)

在这里插入图片描述

  1. 通讯引脚
    SDA、SCL连接到相应的引脚
    在这里插入图片描述
    SMBA是用来做实现SMBA协议,很少使用。

  2. 时钟控制逻辑
    SCL线的时钟信号,由I2C接口根据时钟控制寄存器(CCR)控制,控制的参数主要为时钟频率。
    ■ 可选择I2C通讯的“标准/快速”模式,这两个模式分别I2C对应100/400Kbit/s的通讯速率。
    ■ 在快速模式下可选择SCL时钟的占空比,可选Tlow/Thigh=2 或 Tlow/Thigh=16/9模式。
    ■ CCR寄存器中12位的配置因子CCR,它与12C外设的输入时钟源共同作用,产生SCL时钟。STM32的I2C外设输入时钟源为PCLK1。
    在这里插入图片描述

  3. 数据控制逻辑
    I2C的SDA信号主要连接到数据移位寄存器上,数据移位寄存器的数据来源是数据寄存器(DR)、目标时是目标地址寄存器(OAR) PEC寄存器以及SDA数据线。
    当向外发送数据的时候,数据移位寄存器以“数据寄存器”为数据源,把数据一位一位地通过SDA信号线发送出去;
    当从外部接收数据的时候,数据移位寄存器把SDA信号线采样到的数据一位一位地存储到“数据寄存器”中。

  4. 整体控制逻辑
    整体控制逻辑负责协调整个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_SMBusHostI2C_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外设。可以选择 I2C1I2C2
  • NewState
    通过此来选择是否产生起始位。可以选择 ENABLEDISABLE
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外设。可以选择 I2C1I2C2
  • 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外设。可以选择 I2C1I2C2
  • 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外设。可以选择 I2C1I2C2
  • 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外设。可以选择 I2C1I2C2

应答信号配置函数

/**
 * @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外设。可以选择 I2C1I2C2
  • NewState
    通过此来选择是否产生应答信号。可以选择 ENABLEDISABLE。在通讯计数是,设置为 DISABLE 结束通讯。

在这里插入图片描述

三、软件模拟I2C,读写EEPROM

为啥使用 EEPROM 做IIC实验呢?便宜且使用IIC协议通讯。
EEPROM-AT24C02具体请看 AT24C02简介

开发板上的EEPROM连接电路图:

3.1 软件实现简单读写EEPROM

具体思路:

  1. 首先初始化IIC
    把连接EEPROM的SCL、SDA引脚的GPIO设置为推挽输出。
  2. 然后通过字节写入的方法写入1 byte的数据到eeprom。
    我们要模拟时序先后产生起始信号、写入设备信号、写入数据内存地址信号、写入数据信号、结束信号来实现写数据。
  3. 最后通过指定地址的方法再读取出写入的数据。
    这里我们要模拟时序先后产生起始信号、写入设备信号、写入数据内存地址信号、起始信号、读取设备信号、接收内存数据、产生结束信号。

测试代码:

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是时钟,一定要有周期性。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值