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;
}