3 IIC总线

1、基本概念

1.1 IIC总线定义

定义:两线式串行总线

  • 两线式:说明处理器和外设之间只需两根信号线,分别是SCL时钟控制信号线和SDA数据线

  • SCL:时钟控制信号线,永远只能由CPU控制,用于实现数据的同步,就四个字:低放高取
    SCL为低电平的时候将数据放到SDA数据线上
    SCL为高电平的时候从数据线SDA上获取数据

  • SDA:数据线,用于传输数据,双方都可以控制
    如果处理器给外设发送数据,SDA由处理器控制
    如果外设给处理器发送数据,SDA由外设控制

  • 串行:由于数据线就一根SDA,必然是串行,又由于有时钟控制信号线SCL,所以数据传输是一个时钟周期传输一个bit位

  • 总线:说明SCL和SDA上可以连接多个外设(理论上也可以连接多个CPU,此场景极其少见,常见一个处理器连接多个外设)
    在这里插入图片描述

  • 切记:SCL和SDA必须要分别连接一个上拉电阻,所以他们默认的电平都是高电平,且I2C数据传输从高位开始,I2C数据传输一次传输一个字节,如果传输多个字节,需要分拆着来传
    在这里插入图片描述

  • 主-从架构
    一个设备作为主设备 - master - cpu
    发起通信控制总线的时序
    其他设备作为从设备 - slave - 外设
    在cpu的控制下进行响应和数据传输

  • 半双工通信
    允许双向传输,但是在同一个时刻只能单向传输

  • 通信速率
    标准模式 - 100kbps
    快速模式 - 400kbps
    高速模式 - 3.4Mbps

1.2 IIC总线协议概念

  • 寻址机制
    在同一条总线上,每个IIC设备都有一个唯一的7位作为设备地址,设备地址由原理图和芯片手册共同决定在这里插入图片描述

  • R/W读写位:用于表示CPU到底是向外设写入数据还是从外设读取数据,有效位数为1个bit位,CPU读取数据:R/W=1,CPU向外设写入数据:R/W=0

  • START信号:又称起始信号,此信号永远只能由CPU发起,表示CPU开始要访问外设时序为:SCL为高电平,SDA由高电平向低电平跳变产生START信号

  • STOP信号:又称结束信号,此信号永远只能由CPU发起,表示CPU结束对总线的访问时序为:SCL为高电平,SDA由低电平向高电平跳变产生STOP信号
    在这里插入图片描述

  • ACK信号:又称应答信号,表示双方数据传输的反馈,有效位数为1个bit位,低电平有效

  • nack/NAK信号:有效位是1个bit位,高电平有效,只在特定情况下使用

  • 片内寄存器:任何I2C外设芯片内部都集成了一堆的寄存器,此类寄存器又称片内寄存器
    这些寄存器同样也有地址,地址编号从0x00开始
    虽然这些寄存器都有唯一的地址,但是CPU不能直接以指针的形式访问,必须要严格按照,读写时序进行访问

  • 结论:CPU访问I2C外设本质就是访问I2C外设内部的寄存器!所以I2C外设本身只需关注三点:
    I2C外设片内寄存器的特性
    I2C外设片内寄存器的基地址
    I2C外设片内寄存器的读写时序

2 以AT24C02为例说明时序

2.1 基本特性

1.存储介质
EEPROM - Electrically Eraseable Programmable ROM
无限次擦除和重写数据
大约10万次擦除
2.存储容量 - 256字节 - 2K位
3.数据保持时间
断电保存数据 - 十年以上
4.通信接口
I2C接口 - SDA / SCL
5.工作电压
1.8V - 5.5V之间
6.物理特性
SCL / SDA - 数据传输
WP - 写保护
如果将WP拉高, 将保护引脚WP拉高, 防止芯片被写入/擦除.
7.分页机制
共256字节
256个寄存器, 每个寄存器1字节, 寻址从0x00寻址 0x00 - 0xff
每8个字节一页, 分页管理 共32页
第1页 第2页 第3页 第4页 第5页 第6页 …
|--------|--------|--------|--------|--------|--------|--------
0 8 16 24 32 40 每一页的首地址
8.可以跨页读,不可跨页写
例如:
读取6 7 8 9地址数据, 正常读取
发生跨页写的操作, 会回滚到当前页首
例如:
写入6 7 8 9地址数据
6 7 - 正常写入
0 1 - 写入
6 7 0 1 - 写入的寄存器
9.读写时序

  • 写入单字节
    在这里插入图片描述
[CPU]发送开始信号 
[CPU]发送写设备地址 
[外设]回复ack
[CPU]发送要写入的寄存器地址 
[外设]回复ack
[CPU]发送要写入的数据(1字节)
[外设]回复ack
[CPU]发送停止信号
  • 写多字节
    在这里插入图片描述
[CPU]发送开始信号 
[CPU]发送写设备地址 
[外设]回复ack
[CPU]发送要写入的寄存器地址n
[外设]回复ack 
[CPU]发送数据1 -> 地址为n寄存器中 
[外设]回复ack 
[CPU]发送数据2 -> 地址为n+1寄存器中
[外设]回复ack 
[CPU]发送数据3 -> 地址为n+2寄存器中
[外设]回复ack
....
[CPU]发送数据 -> 地址为n+x寄存器中
[外设]回复ack
[CPU]发送停止信号
  • 读取单字节:
    在这里插入图片描述
[CPU]发送开始信号 
[CPU]发送写设备地址 
[外设]回复ack
[CPU]发送要读取的寄存器地址n 
[外设]回复ack
[CPU]发送开始信号 
[CPU]发送读设备地址 
[外设]回复ack
[外设]发送寄存器n的数据给CPU
[CPU]回复nack 
[CPU]发送停止信号 
  • 读取多字节
    在这里插入图片描述
[CPU]发送开始信号 
[CPU]发送写设备地址 
[外设]回复ack
[CPU]发送要读取的寄存器地址n 
[外设]回复ack
[CPU]发送开始信号 
[CPU]发送读设备地址 
[外设]回复ack
[外设]发送寄存器n的数据给CPU
[CPU]回复ack
[外设]发送寄存器n+1的数据给CPU
[CPU]回复ack
[外设]发送寄存器n+2的数据给CPU
[CPU]回复ack
...
[外设]发送寄存器n+x的数据给CPU
[CPU]回复nack 
[CPU]发送停止信号 

2.2 利用GPIO模拟IIC

在system目录下,新建IIC目录,打开keil工程,新建iic.c和iic.h文件
编辑iic.h

#ifndef __IIC_H_
#define __IIC_H_
#include "stm32f10x.h"
#include "system.h"
// 定义SCL和SDA 引脚信息
#define IIC_SCL_PORT GPIOB
#define IIC_SCL_PIN GPIO_Pin_6
#define IIC_SCL PBout(6)
// SDA GPIOB7
#define IIC_SDA_PORT GPIOB
#define IIC_SDA_PIN GPIO_Pin_7
#define IIC_SDA PBout(7)
#define READ_SDA PBin(7)
// 函数声明
void 	IIC_Init(void);// IIC初始化
void 	IIC_Start(void); // 开始信号
void 	IIC_Stop(void); // 结束信号
u8 		IIC_Wait_Ack(void); // 等待响应
void    IIC_NAck(void); // NAck信号
void	IIC_Ack(void); // 应答信号
void 	IIC_Send_Byte(u8 TxByte); // 发送数据
u8 		IIC_Read_Byte(u8 ack); // 读取数据
#endif

编辑iic.h

#include "iic.h"
#include "systick.h"
/*
	时钟线和数据线的初始化
*/
void IIC_Init(void){
	// 打开时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	// SCL PB6
	GPIO_InitTypeDef gpio_init;
	gpio_init.GPIO_Mode = GPIO_Mode_Out_PP;
	gpio_init.GPIO_Pin = IIC_SCL_PIN;
	gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(IIC_SCL_PORT,&gpio_init);
	
	// SDA PB7
	gpio_init.GPIO_Pin = IIC_SDA_PIN;
	gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(IIC_SDA_PORT,&gpio_init);
	
	// 将SCL和SDA拉高
	IIC_SCL =1;
	IIC_SDA =1;
}

// 配置SDA为输入模式
void SDA_IN(void){
	// SDA PB7
	GPIO_InitTypeDef gpio_init;
	gpio_init.GPIO_Mode = GPIO_Mode_IPU;
	gpio_init.GPIO_Pin = IIC_SDA_PIN;
	gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(IIC_SDA_PORT,&gpio_init);
}

// 配置SDA为输出模式
void SDA_OUT(void){
	// SDA PB7
	GPIO_InitTypeDef gpio_init;
	gpio_init.GPIO_Mode = GPIO_Mode_Out_PP;
	gpio_init.GPIO_Pin = IIC_SDA_PIN;
	gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(IIC_SDA_PORT,&gpio_init);
}

根据开始信号时序,书写开始信号函数
在这里插入图片描述

在这里插入图片描述

编辑iic.c

/**
	IIC的操作函数
*/
// 开始信号
void IIC_Start(void){
	IIC_SCL = 0;
	SDA_OUT();
	IIC_SCL=1;
	IIC_SDA=1;
	// 1、时钟和数据线拉高,保持至少4.7us
	delay_us(6);
	IIC_SDA=0;
	// 2、时钟线拉高,数据线拉低,至少保持5us
	delay_us(6);
}

// 结束信号
void IIC_Stop(){

	IIC_SCL=0;
	SDA_OUT();
	IIC_SDA=0;
	IIC_SCL=1;
	// 时钟线拉高,数据线拉低,至少保持4us
	delay_us(6);
	IIC_SDA = 1;
	// 时钟线拉高,数据线拉高,至少保持4.7us
	delay_us(6);
}

// 等待ACK信号
// @return 如果收到了ack,返回0,没有收到ack,返回-1 
// 外设发ack,cpu收ack   时序为低放高取
u8 IIC_Wait_Ack(void){
	int tempTime = 0;
	// 1 将SCL拉低,让让外设将数据放入SDA上
	IIC_SCL = 0;
	// 1.1 让时钟线拉低保持至少4.7us
	delay_us(6); 
	// 1.2 配置SDA为输入
	SDA_IN();// 从外设读取ACK信号
	IIC_SCL=1;
	// 2 将SCL拉高,用于读取ACK信号
	delay_us(6);
	// 2.1 检查ACK信号,收到了ACK,值为0,没收到ACK信号,则READ_SDA=1;循环等待
	while(READ_SDA){
		tempTime++;
		if(tempTime>250){
			IIC_Stop();
			return -1;
		}
	}
	IIC_SCL = 0; // 收到了ack,这里拉低,是为了方便下一次的数据传输(这里必须要拉低)
	return 0;
}
// 发送nack信号  低放高取
void  IIC_NAck(void){
	// 将scl拉低,将数据放入sda上
	IIC_SCL = 0;
	SDA_OUT();
	IIC_SDA = 1; // 将SDA拉高,发送nack信号
	// 保持6us
	delay_us(6);
	// 将SCL拉高,用于外设读取数据
	IIC_SCL =1;
	delay_us(6);
	IIC_SCL = 0; // 继续后续的数据传输
}
// 发送Ack信号
void IIC_Ack(void){
	// 将scl拉低,将数据放入sda上
	IIC_SCL=0;
	SDA_OUT();
	IIC_SDA = 0; // 将SDA拉低,发送ack信号
	// 保存6us
	delay_us(6);
	// 将SCL拉高,用于外设读取数据
	IIC_SCL =1;
	delay_us(6);
	IIC_SCL =0;
}

// 发送单字节 IIC是先发送高位后发送低位
void IIC_Send_Byte(u8 TxByte){
	int i=0;
	IIC_SCL=0;// 拉低,为了将数据放入SDA
	SDA_OUT();
	for(i=0;i<8;i++){
		IIC_SCL=0;// 拉低,为了将数据放入SDA
		IIC_SDA = (TxByte>>(7-i))&0x01;// 将数据放到SDA上 
		delay_us(6);
		// 将SCL拉高
		IIC_SCL = 1;
		delay_us(6);
	}
}

// 接收单字节
// @return 返回读到的数据
// @param ack 1回复ack ack 0 回复nack
u8 IIC_Read_Byte(u8 ack){
	u8 i =0,data =0;
	IIC_SCL = 0;// 拉低SCL 让外设放数据
	SDA_IN();
	for(i=0;i<8;i++){
		IIC_SCL = 0;// 拉低SCL 让外设放数据
		delay_us(6);
		IIC_SCL = 1;// 拉高SCL cpu读取数据
		data |= READ_SDA <<(7-i);
	}
	// 判断读取了1字节的数据后, 回复ack还是nack
	if(!ack)
		IIC_NAck();
	else
		IIC_Ack();
	return data; // 返回读取到的数据
}	

2.3 对AT24C02的操作

在system目录下,新建AT24C02目录,打开keil工程,新建at24c02.c和at24c02.h文件
编辑at24c02.h

#ifndef __AT24C02_H_
#define __AT24C02_H_
#include "stm32f10x.h"

// 外设地址
#define AT24C02_ID (0x50)
// 功能函数
void 	AT24C02_Init(void);
u8 		AT24C02_ReadByte(u16 ReadAddr);
void 	AT24C02_WriteByte(u16 WriteAddr,u8 data);
void 	AT24C02_ReadBlockData(u16 ReadAddr,u8* pBuffer,u16 Len);
void 	AT24C02_WriteBlockData(u16 WriteAddr,u8* pBuffer,u16 Len);

// 测试函数
void 	AT24C02_ReadOne(void);
void 	AT24C02_WriteOne(void);
void 	AT24C02_ReadMul(void);
void 	AT24C02_WriteMul(void);
#endif

编辑at24c02.c

#include "at24c02.h"
#include "iic.h"

#include "systick.h"
#include "stdio.h"

/** 功能函数 */
// 初始化函数
void 	AT24C02_Init(void){
	IIC_Init();
}

根据上文提到了AT24C02的时序图,完成单字节、多字节数据的读取与写入

// 定义读取一个字节的函数
u8 AT24C02_ReadByte(u16 ReadAddr){
	u8 temp;
	// 1 发送开始信号
	IIC_Start();
	// 2 发送写设备地址
	IIC_Send_Byte(AT24C02_ID<<1|0);
	// 3 等待ACK信号
	IIC_Wait_Ack();
	// 4 发送要读取的寄存器地址
	IIC_Send_Byte(ReadAddr);
	// 5 等待ACK信号
	IIC_Wait_Ack();
	// 6 发送开始信号
	IIC_Start();
	// 7 发送读设备地址
	IIC_Send_Byte(AT24C02_ID<<1|1);
	// 8 等待ack信号
	IIC_Wait_Ack();
	// 9 读取单字节数据+回复nack信号
	temp = IIC_Read_Byte(0);
	// 10 发送stop信号
	IIC_Stop();
	// 11 返回数据
	return temp;
}
// @brief 写入的寄存器地址
// @param 写入的数据
void AT24C02_WriteByte(u16 WriteAddr,u8 data){
	// 1 发送开始信号
	IIC_Start();
	// 2 发送写设备地址
	IIC_Send_Byte(AT24C02_ID << 1 | 0);
	// 3 等待ACK信号
	IIC_Wait_Ack();
	// 4 发送要写入的寄存器地址
	IIC_Send_Byte(WriteAddr);
	// 5 等待ACK信号
	IIC_Wait_Ack();
	// 6 发送要写入的数据
	IIC_Send_Byte(data);
	// 7 等待ACK
	IIC_Wait_Ack();
	// 8 发送结束信号
	IIC_Stop();
}

// @brief 读取多个字节
// @param 
//		readAddr 地址
// 		pBuffer 读取数据存储的存储区,第一个寄存器地址
// 		Len 读取的字节个数
void AT24C02_ReadBlockData(u16 ReadAddr,u8* pBuffer,u16 Len){
	// 方法1
	/*
	while(Len){
		*pBuffer++ = AT24C02_ReadByte(ReadAddr++);
		Len--;
	}
	*/
	
	// 方法2
	// 发送开始信号
	IIC_Start();
	// 发送写设备地址
	IIC_Send_Byte(AT24C02_ID<<1|0);
	// 等待ACK信号
	IIC_Wait_Ack();
	// 发送要读取的寄存器地址
	IIC_Send_Byte(ReadAddr);
	// 等待ACK信号
	IIC_Wait_Ack();
	// 发送开始信号
	IIC_Start();
	// 发送读设备地址
	IIC_Send_Byte(AT24C02_ID<<1|1);
	// 等待ack信号
	IIC_Wait_Ack();
	while(Len){
		if(Len==1){
			*pBuffer = IIC_Read_Byte(0);
		}else{
			*pBuffer = IIC_Read_Byte(1);
		}
		pBuffer++;
		Len--;
	}
	IIC_Stop();
}

// @brief 写入多个字节
// @param 
//		WriteAddr 地址
// 		pBuffer 写入数据存储的存储区,第一个寄存器地址
// 		Len 写入的字节个数
void AT24C02_WriteBlockData(u16 WriteAddr,u8* pBuffer,u16 Len){
	// 方法1
	/*
	while(Len){
		AT24C02_WriteByte(WriteAddr,*pBuffer);
		pBuffer++;
		WriteAddr++;
		Len--;
	}
	*/
	
	// 方法2
	IIC_Start();
	// 发送写设备地址
	IIC_Send_Byte(AT24C02_ID<<1|0);
	// 等待ACK信号
	IIC_Wait_Ack();
	// 发送要写入的寄存器地址
	IIC_Send_Byte(WriteAddr);
	// 等待ACK信号
	IIC_Wait_Ack();
	printf("1");
	while(Len--){
		IIC_Send_Byte(*pBuffer);
		IIC_Wait_Ack();
		WriteAddr++;
		pBuffer++;
		printf("1");
		if(WriteAddr % 8 ==0){
			IIC_Stop();// 结束传输
			delay_ms(5);//写页需要5ms
			// 重新传输
			IIC_Start();
			// 发送写设备地址
			IIC_Send_Byte(AT24C02_ID<<1|0);
			// 等待ACK信号
			IIC_Wait_Ack();
			// 发送要写入的寄存器地址
			IIC_Send_Byte(WriteAddr);
			// 等待ACK信号
			IIC_Wait_Ack();
		}
	}
	IIC_Stop();
	delay_ms(5);
}

// 测试函数
void AT24C02_ReadOne(void){
	// 读取0x00寄存器,显示
	printf("Read data : %#x\r\n",AT24C02_ReadByte(0x10));
}
void AT24C02_WriteOne(void){
	// 将数据0xff写入0x00寄存器中
	AT24C02_WriteByte(0x10,0X68);
	printf("Write data : \r\n");
}
void AT24C02_ReadMul(void){
	u8 data[5]={0};
	AT24C02_ReadBlockData(0x00,data,5);
	for(int i=0;i<5;i++){
		printf("adde[%d]:data[%#x]\r\n",i,data[i]);
	}
}
void AT24C02_WriteMul(void){
	// 将数据1 2 3 4 5分别写入地址00 01 02 03 04寄存器中
	u8 data[5] = {0x44,0x45,0x46,0x47,0x48};
	AT24C02_WriteBlockData(0x00,data,5);
}

这里需要重定向printf,使printf中的内容重定向到串口中

2.4 重定向printf

在printf函数中会调用fputc函数,在串口uart.c中重写fputs函数

#include <stdio.h>
// 重定向printf函数,后续printf函数自动调用该函数
// @param c:要发送的字符
int fputc(int c,FILE* fp){
	// 判断发送缓冲区是否为空
	while(USART_GetFlagStatus(USART1,USART_FLAG_TC)== RESET); 
	// 获取要发送的字符,就将其发送到串口1中
	USART_SendData(USART1,(u8)c);
	return c;
}
  • 36
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

启航zpyl

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

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

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

打赏作者

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

抵扣说明:

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

余额充值