目录
I2C(Inter-Intergrated Circuit)和USART比较:
用寄存器的方式实现:提醒原理和寄存器都是一样的具体的引脚配置要看你选中的32单片机I2C复用引脚是哪些,改变代码的引脚即可运行
I2C的初始化:I2C用到的寄存器解读(从I2C功能框图中可以看到)
I2C_CCR:CCR寄存器只有在关闭I 2 C时(PE=0)才能设置:
前言:出软件模拟的目的是为了加深对I2C的理解,硬件类用HAL库是最好的即避免了寄存器配置上的误失导致无法实现业务又能高效处理事务,但是为什么我还要出寄存器的实现方式能因为32单片机后续出了新的板子都没有再更新HAL库了所以我建议大家都会用硬件配置
I2C(Inter-Intergrated Circuit)和USART比较:
特性 | I2C | USART(UART模式) |
---|---|---|
同步机制 | SCL时钟同步 | 异步(依靠起始/停止位) |
连接方式 | 总线拓扑(多主多从) | 点对点 |
引脚占用 | 2线 | 至少3线(TX/RX/GND) |
传输速率 | 标准模式100kHz,快速模式400kHz | 最高115200bps |
应用场景 | 传感器、EEPROM等外设 | 通用异步串行通信 |
I2C协议的技术优势与通信机制解析
I2C协议凭借其双线制设计(SCL时钟线+SDA数据线)成为嵌入式系统的"效率担当",特别适合低速外设互联场景。与USART的异步四线制(TX/RX分离)不同,I2C采用半双工模式,单线双向传输数据,硬件上仅需两个引脚即可构建通信网络。例如STM32的USART1接口若接APB2总线可达72MHz,但I2C在100Kbit/s标准速率下即能满足多数传感器、EEPROM等低速设备的通信需求,硬件复杂度显著降低。
在硬件实现层面,I2C通过开漏输出配合上拉电阻巧妙地解决了多设备共享总线的冲突问题。主设备作为"指挥官"掌控SCL时钟线,从设备则通过唯一地址响应主设备召唤。当多个主设备同时申请总线时,采用"线与"机制:空闲状态下所有设备输出高阻态(相当于逻辑1),任一设备拉低SDA即宣告占用总线。从设备若因处理速度不足,还可通过时钟拉伸(主动拉低SCL)请求主设备降速,这种协作机制确保了多主多从架构的稳定性。
协议设计方面,I2C通过精准的边沿控制实现通信同步。起始信号由SDA在SCL高电平时的下降沿触发,停止信号则为上升沿。数据帧采用7位地址+1位读写方向位(LSB)结构,支持7/10位双模式寻址。与USART的LSB优先传输不同,I2C采用MSB优先,每个字节传输后接收方需通过拉低SDA发送ACK应答,否则发送方将收到NACK信号并终止传输。这种应答机制配合高电平采样规则(SCL高电平时读取数据),在保持总线效率的同时,确保了多设备环境下的通信秩序。
硬件设备选择:
EEPROM芯片最常用的通讯方式就是I2C协议。我们使用的32单片机以I2C模式和EEPROM(这里选用M24C02型号)通信(读写操作)
EEPROM(M24C02型号)的硬件介绍:
M24C02这个EEPROM芯片啊,虽然只有8个脚,但挺能装的,有256个字节的存储空间,也就是2048个位。它的存储结构挺有意思的,像个小本子,每页有16个字节,总共16页。要找数据的时候,用8位地址就行,前4位是页码,后4位是页里的具体位置。
写入数据的时候,不管是单个字节还是整页写,都得等5毫秒,就像等红灯一样,得让芯片把数据存稳了才能进行下一步。这种设计啊,虽然写入速度不算快,但挺适合那些不需要频繁改数据的应用场景。
在I²C总线接口方面,M24C02支持7位或10位设备地址模式,其地址字段前三位固定为'101',后续位通过硬件引脚配置(A2/A1/A0)实现多设备级联。器件的写控制引脚(WC#)用于保护数据,高电平时禁止写入,低电平时允许读写操作。I²C通信通过SCL(串行时钟)和SDA(串行数据)双线实现,总线需接上拉电阻以确保空闲状态的高电平,这是I²C总线电气特性的标准配置要求。
定义设备一个写地址:1010 0000;读取1010 0001(注意软件模拟的时候要用到其底层内容)
EEPROM连接STM32的原理图:
连接STM32的原理图,注意PB10,PB11是有复用功能的引脚
在I²C总线系统中
若需实现双向收发功能且不使用专用I²C外设,可将GPIO配置为通用开漏输出模式。由于I²C总线默认通过上拉电阻保持高电平,设备只需输出低电平(0)即可主动控制总线状态,高电平状态则由上拉电阻被动实现。这种配置方式简化了硬件设计,但需严格遵循I²C协议的时序规范;
若使用专用I²C外设,那么PB10和PB11就是复用开漏输出,配置GPIO引脚为I2C功能。
I2C协议具体时序图:
EEPROM在I2C协议中和外设通信图:
(主设备32,EEPROM从设备)在硬件设备中WC#被拉低(器件的写控制引脚(WC#)用于保护数据,高电平时禁止写入,低电平时允许读写操作),所以我们就考虑写入字节,先给开始字节,然后是设备地址码(7位地址位1位读写方向位,0为写入,1为读出)接着是接收设备的响应,然后发EEPROM设备内的内部地址(8位,就像动态变量),接着EEPROM再响应,然后就传递真正要发送的数据,从设备再给个应答信号,一般有5ms的写入处理时间,后再处理其他事情,发送完毕后主设备发送结束信号即可(可以看出数据处理复杂,适用与低速数据传输)
如果连续写入多个字节(也叫页写入,页只有16个字节,所以最多只能写16个字节,如果一次性写入16可以但是EEPROM会任然停留再该页,绕到开头覆盖开头的数据):则多一个发一段数据,主设备发响应,从设备再发数据,主设备再发响应....发一个响应一个,每次发完等5ms,即写入完成返回ACK再发下一个,主设备不发就给一个STOP即可
电脑通过串口控制32给EEPROM写入设备地址,接着从设备响应,主设备再发一个从设备内部的设备地址(存储寄存器等),从设备响应,主设备再重新发设备地址码然后做一个读操作(写1),响应后从设备拿到数据总线将Byte address内部读取到的数据向主设备发收,主设备直接给一个不应答,从设备知道没收到就不敢发,那么主设备就发一个STOP信号(上升沿),从设备就不发了
读取多个字节时序:同理,注意读多个字节没有限制
软件模拟只能用寄存器,因为要手动模拟!!!
软件模拟实现代码:
底层I2C协议的实现:
#include "I2C.h"
#ifndef __I2C_H
#define __I2C_H
#include "stm32f10x.h"
//引入延时,时钟信号拉低电平后延时再改变时钟信号目的是实现数据信号的改变,但是延时是阻塞的,那么我的数据信号怎么改变呢?
//宏定义
//应答
#define ACK 0;
#define NACK 1;
//读写数据
//#define write 0;
//#define read 1;
//针对32单片机的操作信号线拉高拉低,本质是引脚PB10(SCL),PB11(SDA)的输出电平,相当于32控制时钟线和数据线
//拉高
#define SCL_HIGH (GPIOB->ODR |= GPIO_ODR_ODR10)
//拉低
#define SCL_LOW (GPIOB->ODR &= ~GPIO_ODR_ODR10)
//SDA拉高拉低(32作为主设备发送数据。和读取EEPROM的数据,但是我们给PB11设置的开漏输出,又因为I2C通信上是有上拉电阻的,输出的信号只有0,那为什么只设置SDA_LOW,如果不写数据了就取反好了,嗯!突然想起不可以这么理解,因为再SCL都是高电平的时候只要不是跳变都是指的发送数据,所以为了好分辨是要设置一个拉高的定义的)
//拉高
#define SDA_HIGH (GPIOB->ODR |= GPIO_ODR_ODR11)
//拉低
#define SDA_LOW (GPIOB->ODR &= ~GPIO_ODR_ODR11)
//读取EEPRON数据,即EEPROM向32发数据
#define READ_SDA(GPIOB->IDR &GPIO_IDR_IDR11)
//基本延时,我在想串口发送的时候不会受到主程序运行的影响吗,它发还是在发是底层硬件的操作,我软件操作在不限制硬件的时候是不会有影响的是吗?我延时的时候没有影响硬件,哦,我想起里USART往串口发送了多条数据,串口是收到了,但是我软件还在走亮灯的那个操作
#define I2C_DELAY Delay_us(10);
//我们做出的标准操作都是基于32单片机的角度,因为代码是要烧到单片机上的
//初始化
void I2C_Init(void);
//发出起始信号
void I2C_Start(void);
//发出停止信号
void I2C_Stop(void);
//32和EEPROM和数据进行操作时,收到数据还要给一个响应信号,可是我都已经宏定义了应答信号和32输出信号的拉低位了,为什么我不直接用还要给个函数呢?为了读取方便和明确操作目的!!!
void I2C_ACK(void);
//非应答
void I2C_Nack(void);
//从机也会给32发应答和信号那么主机就要接收从机的信号,如果它发了应答那我就做操作A,不应答就做操作B,因此主机也是要等待接收从机信号才做下一步操作
uint8_t I2C_Wait4ACK(void);
//主机发送数据(注意主机发送从设备地址,从设备内部空间地址,操作数据,对于底层来说都是数据,我们就将它封装再一个函数中)
//主机发送一个字节的数据(写入)
void I2C_SendByte(uint8_t byte);
//主机接收一个字节的数据(读取)
uint8_t I2C_ReadByte(void);
#endif
//初始化
void I2C_Init(void){
//时钟PB10.PB11
RCC->APB2ENR |=RCC_APB2_APB2ENR_IOPBEN;
//GPIO是时钟配置:允许时钟扩展,即允许当从设备跟不上时钟信号就拉低通知主设备,注意主设备是32板子
//所以SDA和SCL都是双向的,通用开漏输出CNF-01,MODE-11
GPIOB->CRH | = (GPIO_CRH_MODE10|GPIO_CRH_MODE11);
GPIOB->CRH & = ~(GPIO_CRH_CNF10_1|GPIO_CRH_CNF11_1);
GPIOB->CRH | = (GPIO_CRH_CNF10_0|GPIO_CRH_CNF11_0);
}
||
//发出起始信号
void I2C_Start(void)
{
//1.SCL拉高,SDA初始拉高
SCL_HIGH;
SDA_HIGH;
I2C_DELAY;
//2.SCL保持不变,SDA拉低
SDA_LOW;
I2C_DELAY;
}
//发出停止信号
void I2C_Stop(void)
{
//1.SCL拉高,SDA初始拉高
SCL_HIGH;
SDA_LOW;
I2C_DELAY;
//2.SCL保持不变,SDA拉低
SDA_HIGH;
I2C_DELAY;
}
主机发出应答和非应答信号:
//主机发出应答信号
void I2C_Ack(void){
//1.SCL拉低,SDA也是拉高初始
SCL_LOW;
SDA_HIGH;
I2C_DEALY;
//2.响应
SDA_LOW;
I2C_DELAY;
//3.SCL跳变到高开始采样
SCL_HIGH;
I2C_DELAY;
//结束数据线上数据线采样
SCL_LOW;
I2C_DELAY;
//主设备释放SDA数据线,给从设备使用
SDA_HIGH;
I2C_DELAY;
}
//非应答
void I2C_NAck(void){
//1.SCL拉低,SDA也是拉高初始
SCL_LOW;
SDA_HIGH;
I2C_DEALY;
//2.不响应
SDA_HIGH;
I2C_DELAY;
//3.SCL跳变到高开始采样
SCL_HIGH;
I2C_DELAY;
//结束数据线上数据线采样
SCL_LOW;
I2C_DELAY;
//主设备释放SDA数据线,给从设备使用
SDA_HIGH;
I2C_DELAY;
}
主机等待从设备发出应答或非应答信号
uint8_t I2C_Wait4Ack(void)
{
//主机拉高等待应答,初始状态
SDL_HIGH;//相当于释放数据总线
SCL_LOW;
I2C_DELAY;
//2.SDA持续拉高等待数据,SCL拉高开始采样从机数据
SCL_HIGH;
I2C_DELAY;
//那什么时候表示从机应答完毕呢??所以开始读取信号
uint16_t ack=READ_SDA;//注意再前面我们定义#define READ_SDA(GPIOB->IDR &GPIO_IDR_IDR11),如果是应答则返回0,若是非应答呢就会返回IDR寄存器的数据,所以我们可以先判断然后转换成0或1即可
//采样结束,注意虽然图中SDA也有最终拉高的变化,但是我们只写了SCL的原因是,SDA一切数据的获取前提都是SCL为高电平的时候,所以只要SCL拉低,SDA不论怎么变化都是无效数据,因此只改变SCL即可
SCL_LOW;
I2C_DELAY;
return ack?NACK:ACK;//如果ack=0就是应答,如果IDR的一串数据那么就换算成10进制就>0,效果等效于true,所以返回NACK
}
对于1字节的数据的写
//主机发送数据(注意主机发送从设备地址,从设备内部空间地址,操作数据,对于底层来说都是数据,我们就将它封装再一个函数中)
//主机发送一个字节的数据(写入),,,注意串口通信都是先发高位再发低位的!!!,所以就要用位移去实现
void I2C_SendByte(uint8_t byte){
//我用for循环实现一位一位的拿取,且1位字节就是8位数据
for(uint8_t i=0;i<8;i++){
//1.发送数据之前的起始信号
//拉低,等待数据翻转
SCL_LOW;
SDA_LOW;
I2C_DELAY;
//2.取传入字节的最高位,左移就可以实现每次都取最高位发送到SDA,即向SDA写入数据
if(byte & 0x80 )//位与,如果最高位有数据
{
SDA_HIGH;
}
else SDA_LOW;
I2C_DELAY;
//3.SCL拉高,数据采样
SCL_HIGH;
I2C_DELAY;
//SCL拉低,采样结束
SCL_LOW;
I2C_DELAY;
//5.这一位发了,就要将传入的字节左移
byte<<=1;
}
读取一个字节
//主机接收一个字节的数据(读取),注意SDA由从设备控制,主设备控制SCL即可
uint8_t I2C_ReadByte(void){
//所以我们要挨个读,用来保存接收的数据
uint8_t data=0;
//循环处理每一位
for(uint8_t i=0;i<8;i++){
//1.SCL拉低,等待数据翻转
SCL_LOW;
I2C_DELAY;
//2.SCL拉高开始采样
SCL_HIGH;
I2C_DELAY;
//3.读取数据,即SDA上的电平
data<<=1;//先做左移,即新存进来都是低位,第8次就不会移出去了
if(READ_SDA){
data |=0x01;//存入最低位,每次都左移,一位最先发进来的数据是高位的
}
//SCL拉低,结束采样
SCL_LOW;
I2C_DELAY;
}
return data;
}
对32中M24C02芯片的操作代码:
#ifndef __M24C02_H
#define __M24C02_H
#include "i2c.h"
//宏定义
#define W_ADDR 0XA0;
#define R_ADDR 0XA1;
//初始化
void M24C02_Init(void);
//我们调用的上层接口
//向EEPROM写入一个字节
void M24C02_WriteByte(uint8_t innerAddr,uint8_t byte);//内部地址和写入数据
//读取EEPROM一个字节
uint8_t M24CO2_ReadByte(uint8_t innerAddr);//指定地址的读取
//连续写入多个字节(页写)
void M24C02_WriteBytes(uint8_t innerAddr,uint8_t *byte,uint8_t size);//页有16字节的限制
//连续读取多个字节(页写)
void M24C02_ReadBytes(uint8_t innerAddr,uint8_t *buffer,uint8_t size);
#endif
//初始化
void M24C02_Init(void){
//底层用到的就是I2C所以字节调用即可
I2C_Init();
}
//我们调用的上层接口
//向EEPROM写入一个字节
void M24C02_WriteByte(uint8_t innerAddr,uint8_t byte)//内部地址和写入数据
{
//注意WC#芯片中一直都是低电平,所以我们不用给任何操作
//Start信号
I2C_Start();
//写地址,叫I2C发送一个字节的写地址
I2C_SendByte(W_ADDR);
//32等待EEPROM应答
uint_8 ack =I2C_Wait4Ack();
//我这里给出了一个判断是示例,但是往后代码中出现I2C_Wait4Ack()默认==ACK
if(ack==ACK){
//发送内部地址
I2C_SendByte(innerAddr);
I2C_SendByte(byte);//发数据
I2C_Wait4Ack();//等待应答
//最后32发出一个停止信号
I2C_Stop();
}
//延时等待写入周期结束
Delay_ms(5);
}
//注意我这里默认I2C_Wait4Ack()==ACK;
//读取EEPROM一个字节
uint8_t M24CO2_ReadByte(uint8_t innerAddr)//指定地址的读取
{
//开始
I2C_Start();
//写入EEPROM设备地址(假写)
I2C_SendByte(W_ADDR);
I2C_Wait4Ack();
//写入内部地址
I2C_SendByte(innerAddr);
I2C_Wait4Ack();
//开始
I2C_Start();
//发送读取信号
I2C_SendByte(R_ADDR);
I2C_Wait4Ack();
//读取一个字节
uint8_t byte=I2C_ReadByte();
//发送NACK
I2C_Nack();
//停止信号
I2C_Stop();
}
//连续写入多个字节(页写)
void M24C02_WriteBytes(uint8_t innerAddr,uint8_t *byte,uint8_t size)//页有16字节的限制
{
//开始
I2C_Start();
//写入EEPROM设备地址
I2C_SendByte(W_ADDR);
I2C_Wait4Ack();
//写入内部地址
I2C_SendByte(innerAddr);
I2C_Wait4Ack();
//写入多个字节
while(size--){
I2C_WeadByte(*bytes);//另外一种写法bytes[size]
bytes++;
I2C_Wait4Ack();
}
//停止信号
I2C_Stop();
Delay_ms(5);
}
//连续读取多个字节(页写)
void M24C02_ReadBytes(uint8_t innerAddr,uint8_t *buffer,uint8_t size){
//开始
I2C_Start();
//写入EEPROM设备地址(假写)
I2C_SendByte(W_ADDR);
I2C_Wait4Ack();
//写入内部地址
I2C_SendByte(innerAddr);(假写)
I2C_Wait4Ack();
//开始,真读
I2C_Start();
//读EEPROM设备地址
I2C_SendByte(R_ADDR);
//读取多个字节
while(size--){
*buffer=I2C_ReadByte();
buffer++;
I2C_Ack();
}
I2C_Nack();
//停止信号
I2C_Stop();
}
主函数测试
#include "usart.h"
#include "m24c02.h"
int main(void){
UASRT_Init();
M24C02_Init();
//向EEPROM写入单个字符
M24C02_WriteByte(0x00,'a');
/串口输出打印
printf("byte=%c\n",M24C02_ReadByte(0x00));
//uint8_t byte=M24C02_ReadByte(0x00);/串口输出打印
//printf("byte=%c\n",byte);
//向EEPROM写入多个个字符
M24C02_WriteByte(0x00,“haohaoyun”,9);
//读取多个字节
uint8_t *buffer;
M24C02_ReadByte(0x00,buffer,9);/串口输出打印
printf("bytes=%s\n",M24C02_ReadByte(0x00,buffer,9));
//uint8_t *bytes=M24C02_ReadByte(0x00);/串口输出打印
//printf("bytes=%s\n",bytes);
//测试超出16个字节的写入,注意超过16字节页写,EEPROM会绕到起始地址经行数据覆盖的写入
memset(buffer,0,sizeof(buffer));
while(1){
}
硬件实现 :32单片机(主机 )和EEPROM通信(从机)
用HAL库的方式实现:STM32CubeMX+keil5
点开Connectivity可以选择串口通信协议
EEPROM要求我们用I2C2所以点击配置I2C2,我们可以看到配置完后下方自动弹出两个引脚PB10和PB11这就是I2C2的复用引脚
点击下方的Paramemter Settings可以看到关于I2C2的具体参数配置
由于32是主设备给EEPROM写入后读取数据所以我们只需要关注Master Features的配置,Master Features下的I2C Speed Mode是I2C的传输速率,前面我们已经介绍过I2C的时钟频率能以100Kbite/s(标准模式)或400kbit/s(快模式)下,我们这里选用标准模式为示例
Slave Features是32作为从模式的时候需要配置的,这里我们简单介绍下,不仔细展开说明Clock no stretch mode 是用来配置从模式下时钟扩展模式,若跟不上主设备SCL速度就发出放慢速度信号;primary address lengtn是地址寄存器主设备可以通过地址找到32,dual address acknowled 是双地址,primary slave address给我们的32配一个从设备地址,general call address det...通用呼叫,某一个主设备以广播的形式发送信号给32
这样就将I2C底层通信配好了
I2C的HAL库代码(写入和读取数据):
直接根据自己选取的从设备,通过查看其从设备数据手册找到设备地址和设备内部地址再在KEIL5中按参数要求代入即可实现32主设备和从设备进行通信
// 向EEPROM写入字节
HAL_I2C_Mem_Write(
&hi2c2, // I2C句柄(根据实际外设修改,如hi2c2)
EEPROM_DEVICE_ADDRESS, // EEPROM设备地址(7位地址左移1位,如0xA0)
target_mem_address, // 目标内存地址(如0x0000)
I2C_MEMADD_SIZE_8BIT, // 内存地址大小(8位或16位,根据EEPROM规格)
p_data_buffer, // 要写入的数据缓冲区指针
data_length, // 写入数据长度(单位:字节)
HAL_MAX_DELAY // 超时时间(建议使用HAL_MAX_DELAY)
);
// 从EEPROM读取字节
HAL_I2C_Mem_Read(
&hi2c2, // I2C句柄
EEPROM_DEVICE_ADDRESS, // EEPROM设备地址
source_mem_address, // 起始读取地址
I2C_MEMADD_SIZE_8BIT, // 内存地址大小
p_read_buffer, // 读取数据存储缓冲区指针
read_length, // 读取数据长度
HAL_MAX_DELAY // 超时时间
);
用寄存器的方式实现:提醒原理和寄存器都是一样的具体的引脚配置要看你选中的32单片机I2C复用引脚是哪些,改变代码的引脚即可运行
用硬件实现方式和软件实现方式有什么不同呢?注意在软件模拟中PB10和PB11中我们是没有开启I2C时钟信号的,我们只是用到了输出功能来模拟的I2C信号;所以在硬件实现中我们就开启I2C的时钟信号,即启用I2C信号
硬件外设处理I2C的方式减轻了CPU的工作,只要配好外设,就会主动根据协议要求产生通讯信号,收发数据并缓存起来,CPU只要检测该外设的状态和访问数据寄存器就能完成数据的收发
STM32的I2C外设支持100Kbit/s和400Kbit/s的速率,支持7位,10位设备地址,支持DMA数据传输,并具有数据校验功能,它的外设还支持SMBuS2.0协议(系统管理总线协议,使用类似I2C串行通信协议管理内部系统总线,发出各种各样的控制指令。和I2C类似所以可以走I2C通道所以可以使用I2C外设)
关键配置说明:
- 时钟速度:根据器件规格选择(标准模式100kHz/快速模式400kHz)
- 寻址模式:7位地址模式需左移1位(如0xA0→0x140)
- 应答机制:HAL库自动处理ACK/NACK检测
- 超时设置:根据器件响应时间调整(通常100ms足够)
- 写保护:操作前检查WP引脚状态(若硬件支持)
I2C的功能框图:
半双工所以可以是从发送器模式和从接收起模式
I2C功能框图解读:
我选用的芯片和EEPROM连接是用的I2C2引脚复用!!!不是I2C1不可以用,是要看你的单片机外设引脚复用和EEPROM的通信标明了要用什么!!我选用EEPROM就标明了要用I2C2
I2C的初始化:I2C用到的寄存器解读(从I2C功能框图中可以看到)
I2C_CR1:
ACK应答:
注意ACK应答配置后不是立即产生应答反应的,而是在接收的一个字节后返回一个瘾大
STOP和START:
就是配置后立即产生跳变沿的
SMBUS:注意为0时才是开启I2C模式
PE:启动I2C模块
I2C_CR2:
FREQ:
I2C连接的是APB1即低速时钟线上,所以I2C的时钟频率最大36MHz(注意APB1是低速外设总线,APB2是高速外设总线(最大允许72MHz)),因此I2C模块的时钟配置输入时钟频率范围1~36MHz,但是这是针对32单片机I2C应用时的整体时钟,即I2C实际上都达不到36MHz,但是具体收发数据STM32的I2C外设支持100Kbit/s和400Kbit/s的速率,所以要分频
I2C_CCR:CCR寄存器只有在关闭I 2 C时(PE=0)才能设置:
F/S:
默认状态下是标准模式
为什么要配置内因为所有的数据都是要在SCL为高电平时才会被采样,低电平就是用来给SDA一个改变数据的时间的,所以时间设置十分重要,那要怎么配置呢?
我这里以标准模式为例子:100Kbit/s
高电平时间=CCR值*(1/36M)
底层逻辑就是:一个高电平到底包含了多少个底层的时钟周期(APB1配置的低速时钟周期36MHz)?
Thigh和Tlow:我现在要数据传输速率是100Kbit/s,即每秒钟有100kbit采样输出,而整个的周期对应的频率就是100Kbit/s,即周期等于10us,从公式可以看出在标准模式下高低电平的时间是各占一半的,所以高电平周期就是5us
再看TPCLK1:APB1配置的低速时钟周期(1/36MHz)s
所以CCR配置=Thigh/1/36MHz=180
I2C_TRISE:
最大上升沿时间在数据手册里找
1000ns=1us;
我们在上面的例子中选用的RCC=180,TPCLK1=(1/36MHz)s(APB1时钟周期),FREQ=36MHz(APB1时钟频率)所以TRISE=(1us/TPCLK1)+1=FREQ+1
I2C底层的具体实现:
32单片机作为主设备时:产生起始和终止条件:(I2C_CR1S:START,STOP)
等待产生起始信号:一旦发出开始信号,32就变成了主设备,一旦置1就会不断的产生起始条件,为什么呢?因为当切换成主设备前若还有其他信号占用SDA那么,我们就要不停的发送起始信号直到32占用SDA发送了START,就可以给START清0了,那怎么判断呢?就要用到状态寄存器
I2C_SR1:
SB:start bit
产生停止信号:不需要等待,直接发出停止信号即可因为它会在SDA发送完信号后才会产生停止条件
应答信号和非应答信号:
我们可以通过判断:
置0就相当与Nack
置1就相当于Ack
收发数据的处理:用到的寄存器I2C_DR和I2C_SR1
判断DR是否空闲,以便于接收或发送下一个字节
用BTF判断一个字节发送/接收完毕
收发地址的处理:
32单片机作为主模式发送从设备地址是十分重要的所以地址拿出来单独判断
初始化I2C:
要考虑系统时钟,复用引脚,I2C硬件配置(开启工作,工作模式,I2C时钟(时钟频率,高电平,上升沿),使能) !注意为什么配置完系统时钟后还有配置I2C的时钟,因为系统时钟是控制整体STM32的工作,而32单片机的每一个外设都有自己适应的时钟,I2C是外设所以要单独配置时钟
#ifndef __I2C_H
#define __I2C_H
#deine OK 0//收到了正常应答
#define FAIL 1//没有收到正常
//初始化
void I2C_Init(void);
//返回值判断是否发出开始信号成功
uint8_t I2C_Start(void);
//主模式下不需要判断停止信号,直接发就行
void I2C_Stop(void);
//32和EEPROM和数据进行操作时,收到数据还要给一个响应信号,可是我都已经宏定义了应答信号和32输出信号的拉低位了,为什么我不直接用还要给个函数呢?为了读取方便和明确操作目的!!!
使能应答信号
void I2C_ACK(void);
//使能非应答信号,将ACK位关了就表达非应答
void I2C_Nack(void);
//主机发送数据(注意主机发送从设备地址,从设备内部空间地址,操作数据,对于底层来说都是数据,我们就将它封装再一个函数中)
//发送设备地址并等待应答
void I2C_SendAddr(uint8_t Addr);
//主机接收一个字节的数据(读取)
uint8_t I2C_ReadByte(void);
#endif
void I2C_Init(){
//
RCC->APB2ENR |=RCC_APBWENR_IOPBEN;
RCC->APB1ENR |=RCC_APB1ENR_I2C2EN;
//复用开漏输出CNF-11,MOOD-11
GPIOB->CRH | = (GPIO_CRH_MODE10|GPIO_CRH_MODE11|GPIO_CRH_CNF11|GPIO_CRH_CNF10);
//I2C2配置
//硬件工作模式,配置控制寄存器
//开启I2C模式
I2C2->CR1 &=~I2C_CR1_SMBUS;
//开启I2C标准模式,fast and standard,在标准模式下I2C的传输速率100Kbit/s
I2C2->CCR &=~I2C_CCR_FS;
//配置输入的时钟频率,多少兆赫兹就给多少,注意是在APB1外设模块下(最大接收36MHz),所以时钟频率<=36MHz
I2C2->CR2 |=36;
//通过配置高电平时间间接配置通信波特率,其中标准模式下的传输速率100kbit/s,SCL高电平5us,5*36
I2C2->CCR |=180;
//配置SCL上升时间沿最大时钟周期数+1=TRISE
I2C2->TRISE|=37;
//使能I2C2模块,注意要将I2C2的所有基础配置都配好了,才可以使能
I2C2->CR1 |=I2C_CR1_PE;
}
I2C的具体工作信号函数:
//返回值判断是否发出开始信号成功
uint8_t I2C_Start(void){
//直接用控制位置1,控制硬件产生起始信号
//有可能产生起始信号不一定发送出去,数据总线非空闲,所以就要等待直至SDA空闲就置1,所以重复发送
I2C2->CR1 |=I2C_CR1_START;//发送成功后硬件自动清除
//避免一直等待而卡死,所以使用超时时间
uint16_t timeout=0xffff;
//判断开始信号的状态位SB
while(!(I2C->SR1 & I2C_SR1_SB)){//判断的时候不停在读I2C->SR1,发出起始信号紧跟就是写地址写DR,就可以清除SB位
//避免一直等待而卡死,所以使用超时时间
if(timeout--)break;
}
return timeout?OK:FAIL;//当timeout超时就=0,就超时,等于没有发送成功!!
}
//主模式下不需要判断停止信号,直接发就行
void I2C_Stop(void){
//作为主设备,我只负责发,拉高后不管即可
I2C2->CR1 |=I2C2_CR1_STOP;
//有读者可能会想为什么不用判断是否接收数据完毕,因为在CR1寄存器中STOP位置1表明:收到软件信号后硬件只有会在数据接收完毕后才会置1
}
//32和EEPROM和数据进行操作时,收到数据还要给一个响应信号,可是我都已经宏定义了应答信号和32输出信号的拉低位了,为什么我不直接用还要给个函数呢?为了读取方便和明确操作目的!!!
使能应答信号
//主机设置银达信号使能,接收到数据后反馈应答
void I2C_ACK(void){
I2C2->CR1 |= I2C2_CR1_ACK;
}
//使能非应答信号,将ACK位关了就表达非应答
void I2C_Nack(void){
I2C2->CR1 &= ~I2C2_CR1_ACK;//1 0 0,1 1 0
}
用I2C处理具体的数据收发:
//主机发送数据(注意主机发送从设备地址,从设备内部空间地址,操作数据,对于底层来说都是数据,我们就将它封装再一个函数中)
//发送设备地址并等待应答
uint8_t I2C_SendAddr(uint8_t Addr){//ADDR,8字节
//将要发送的地址给DR寄存器,底层硬件会将数据发送出去
I2C2->DR=Addr;
uint16_t timeout=0xffff;
//等待应答信号
while((I2C2->SR1 & I2C_SR1_ADDR)==0)//该位置1表示收到应答,访问I2C2->SR1 就是读取了
{
if(timeout--)break;
}
//注意ADDR是否要清除呢?肯定的,ADDR虽然由硬件自动置1 ,但是不由硬件自动清0,所以要清0;但是怎么清0呢?技术参考手册中表明:在软件读取SR1后,对SR2寄存器的读操作可以清0
//我们访问SR2,为了ADDR清0
I2C->SR2;
return timeout?OK:FAIL;
}
//发送数据
void I2C_SendByte(uint8_t byte)
{
uint16_t timeout=0xffff;
//先等待DR为空,上一个字节数据已经发送完毕
while(!(I2C2->SR1 & I2C_SR1_TXE)){//transmit,发完1,在发0
if(timeout--)break;
}
//将要发送的数据发送出去
I2C2->DR=byte;
//是否发送完毕
timeout=0xffff;
while(!(I2C2->SR1 & I2C_SR1_BTF)){//byte transmit for finsh
if(timeout--)break;
}
//清除BT:两种方式:读SR1后再读或写DR;发起开始或停止信号
//接下来不管是发送或接收数据都会清除BTF,如果是最后一个数据,来了停止条件也会清除它
//我们也可以间接理解为是硬件自动清0(注意只是方便我们记忆,并不是底层逻辑)
}
//主机接收一个字节的数据(读取)
//有读者反馈:读取到的数据喂给32那我怎么处理数据,在哪里处理数据??直接判断引脚数据即可吗?
uint8_t I2C_ReadByte(void)
{
uint16_t timeout=0xffff;
//先等待DR为空,上一个字节数据已经发送完毕
while((I2C2->SR1 & I2C_SR1_RXNE)==0){//receive 收到1,没有0
if(timeout--)break;
}
//将收到的字节返回,如果接收成功返回,没有就返回FAIL
return timeout?I2C2->DR :FAIL;
}
M24C02要完成的操作:
#ifndef __M24C02_H
#define __M24C02_H
#include "i2c.h"
//宏定义
#define W_ADDR 0XA0;
#define R_ADDR 0XA1;
//初始化
void M24C02_Init(void);
//我们调用的上层接口
//向EEPROM写入一个字节
void M24C02_WriteByte(uint8_t innerAddr,uint8_t byte);//内部地址和写入数据
//读取EEPROM一个字节
uint8_t M24CO2_ReadByte(uint8_t innerAddr);//指定地址的读取
//连续写入多个字节(页写)
void M24C02_WriteBytes(uint8_t innerAddr,uint8_t *byte,uint8_t size);//页有16字节的限制
//连续读取多个字节(页写)
void M24C02_ReadBytes(uint8_t innerAddr,uint8_t *buffer,uint8_t size);
#endif
注意写入EEPROM(不论是写字节还是写页)都要等待5ms,是EEPROM数据手册规定的!!!
而读取不用
注意为什么EEPROMH函数调用I2C函数时不处理I2C函数有关超时没有正确接收的返回值,因为该返回值的设置只是希望读者有这样的思考意识,且避免函数进入死循环,所以设置了超时时间;不处理是默认发送成功,一般硬件没有说明问题的话,都是可以发送的!!!!!!
//初始化
void M24C02_Init(void){
//底层用到的就是I2C所以字节调用即可
I2C_Init();
}
//我们调用的上层接口
//向EEPROM写入一个字节
void M24C02_WriteByte(uint8_t innerAddr,uint8_t byte)//内部地址和写入数据
{
//注意WC#芯片中一直都是低电平,所以我们不用给任何操作
//Start信号
I2C_Start();
//写地址,叫I2C发送一个字节的写地址
I2C_SendAddr(W_ADDR);
I2C_SendByte(innerAddr);
I2C_SendByte(byte);//发数据
//最后32发出一个停止信号
I2C_Stop();
}
//延时等待写入周期结束
Delay_ms(5);
}
//连续写入多个字节(页写)
void M24C02_WriteBytes(uint8_t innerAddr,uint8_t *byte,uint8_t size)//页有16字节的限制
{
//开始
I2C_Start();
//写入EEPROM设备地址
I2C_SendAddr(W_ADDR);
//写入内部地址
I2C_SendByte(innerAddr);
//写入多个字节
while(size--){
I2C_WeadByte(*bytes);//另外一种写法bytes[size]
bytes++;
}
//停止信号
I2C_Stop();
Delay_ms(5);
}
32单片机在主模式下接收EEPROM的最后一个数据后要置ACK=0,再发出停止信号
uint8_t M24CO2_ReadByte(uint8_t innerAddr)//指定地址的读取
{
//开始
I2C_Start();
//写入EEPROM设备地址(假写)
I2C_SendAddr(W_ADDR);
//写入内部地址
I2C_SendByte(innerAddr);
//开始信号函数
I2C_Start();
//发送读取信号
I2C_SendByte(R_ADDR);
//读取一个字节
uint8_t byte=I2C_ReadByte();
//等待下一个字节读完后设置NACK
I2C_Nack();
//设置在接收下一个字节后发出停止信号
I2C_Stop();
retrun byte;
}
//注意应答信号已经再发送数据的函数中写入了
//连续读取多个字节(页写)
void M24C02_ReadBytes(uint8_t innerAddr,uint8_t *buffer,uint8_t size){
//开始
I2C_Start();
//写入EEPROM设备地址(假写)
I2C_SendAddr(W_ADDR);
//写入内部地址
I2C_SendByte(innerAddr);(假写)
//开始,真读
I2C_Start();
//读EEPROM设备地址
I2C_SendByte(R_ADDR);
//读取多个字节
for(uint8_t i=0;i<size;i++)
{
//为什么ACK响应要放在接收数据的前面呢,因为只有当其前一个数据接收完毕了才说明有空间可以接收下一个数据,具体内容会看ACK函数
if(i<size-1){I2C_Ack;}
//NACK和 STOP要提前设置的原因是已经到了最后一个字节的接收,所以提醒板子准备置位NACK,STOP,注意NACK和STOP设置了后并不是立即响应的,而是要等待最后一个直接接收完毕后才会发出,所以要提前设置
else {I2C_Nack;I2C_Stop();}
buffer[i]=I2C_ReadByte();//有读者提出:为什么该行不像接收一个字节那样放在NACK和STOP的前面,因为只需要接收一个字节
}
}
主函数
#include "usart.h"
#include "m24c02.h"
int main(void){
UASRT_Init();
M24C02_Init();
//向EEPROM写入单个字符
M24C02_WriteByte(0x00,'a');
/串口输出打印
printf("byte=%c\n",M24C02_ReadByte(0x00));
//uint8_t byte=M24C02_ReadByte(0x00);/串口输出打印
//printf("byte=%c\n",byte);
//向EEPROM写入多个个字符
M24C02_WriteByte(0x00,“haohaoyun”,9);
//读取多个字节
uint8_t *buffer;
M24C02_ReadByte(0x00,buffer,9);/串口输出打印
printf("bytes=%s\n",M24C02_ReadByte(0x00,buffer,9));
//uint8_t *bytes=M24C02_ReadByte(0x00);/串口输出打印
//printf("bytes=%s\n",bytes);
//测试超出16个字节的写入,注意超过16字节页写,EEPROM会绕到起始地址经行数据覆盖的写入
memset(buffer,0,sizeof(buffer));
while(1){
}