1. 软件模拟I²C
需求说明:我们向E2PROM写入一段数据,再读取出来,最后发送到串口,核对是否读写正确。
为了逻辑清晰,约定:在 发送起始信号后 ~ 发送结束信号前,使用完SCL后都将其拉低。
由于STM32时钟过快,EEPROM没有那么快的接收速度,所以我们选择EEPROM的100kHz传输速率。
因为GPIOB在APB2高速外设总线上,时钟频率达到72MHz,远超EEPROM能接受的100kHz,所以需要使用延时来延缓传输速率。故每次改变SDA或SCL都要延时10us。
1.1. 软件实现协议
1.1.1. 起始信号和结束信号
1.1.1.1. 起始信号
规定:当SCL为高电平时,SDA从高电平转变为低电平,则成功发送起始信号。
/**
* @brief 发送起始信号
*
*/
void I2C_Start(void)
{
/* 1. 先将SDA和SCL拉高, 注意SDA先拉高 */
SDA_1;
SCL_1;
I2C_DELAY;
/* 2. 拉低SDA, 发送起始信号 */
SDA_0;
I2C_DELAY;
/* 3. 遵守约定, 中间行为最后都将SCL拉低 */
SCL_0;
I2C_DELAY;
}
注意:一定要在SCL拉高前先拉高SDA,因为此时不知道SDA电平。若SDA此时为低电平,SCL先拉高,再拉高SDA,这样就先发送了一个结束信号,虽然对结果没什么影响,但会让程序时序混乱。
1.1.1.2. 结束信号
规定:当SCL为高电平时,SDA从低电平转变为高电平,则成功发送结束信号。
/**
* @brief 发送结束信号
*
*/
void I2C_Stop(void)
{
/* 1. 按照约定SCL一定为0, 但SDA值未知, 故先将SDA拉低 */
SDA_0;
I2C_DELAY;
/* 2. 拉高SCL, 等待SDA拉高发送结束信号 */
SCL_1;
I2C_DELAY;
/* 3. 拉高SDA, 发送结束信号 */
SDA_1;
I2C_DELAY;
}
1.1.2. 响应 Ack或NAck
1.1.2.1. 响应Ack
以32为主视角,每当从机向我们发送完一个字节数据后,我们都需要响应一个ACK,即把SDA拉低。在响应完Ack后再释放SDA。
/**
* @brief 响应对方Ack
*
*/
void I2C_Ack(void)
{
/* 1. 按照约定SCL一定为0, 将SDA拉低 */
SDA_0;
I2C_DELAY;
/* 2. 将SCL拉高, 响应对方ACK */
SCL_1;
I2C_DELAY;
/* 3. 响应结束, 按照约定拉低SCL */
SCL_0;
I2C_DELAY;
/* 4. 释放SDA, 使其处于空闲状态 */
SDA_1;
I2C_DELAY;
}
注意:若最后不释放SDA,从机将无法向主机发送数据。将SDA拉高释放时,从机才能使用SDA向我们发送数据。SDA不释放代表占用。
1.1.2.2. 响应NAck
当接受数据失败或不想继续接受数据时,向对方响应NAck。
/**
* @brief 响应对方NAck
*
*/
void I2C_NAck(void)
{
/* 1. 按照约定SCL一定为0, 将SDA拉高 */
SDA_1;
I2C_DELAY;
/* 2. 将SCL拉高, 响应对方NACK */
SCL_1;
I2C_DELAY;
/* 3. 响应结束, 按照约定拉低SCL */
SCL_0;
I2C_DELAY;
}
1.1.3. 等待对方响应
每发送完一个字节数据,都要等待对方响应Ack或NAck。
/**
* @brief 等待对方响应[Ack or NAck]
*
*/
uint8_t I2C_Wait4Ack(void)
{
uint16_t ack = 0;
/* 1. 按照约定SCL一定为0, 先释放SDA */
SDA_1;
I2C_DELAY;
/* 2. 拉高SCL锁定SDA, 读取对方响应结果 */
SCL_1;
I2C_DELAY;
ack = READ_SDA;
/* 3. 拉低SCL, 对方释放SDA */
SCL_0;
I2C_DELAY;
/* 4. 若ack为0, 返回Ack, 若不为0, 返回NAck */
return ((ack == 0) ? ACK : NACK);
}
1.1.4. 数据有效性
当SCL为高电平时,除起始信号和结束信号外,此时SDA的值锁定不能变化;当SCL为低电平时,SDA可以任意变化,以此来改变自身的电平。
当SCL为高电平时,发送端会将此时SDA的电平发送给接收方,即:若SDA此时为高点平,就发送一个高电平;若SDA此时为低电平,就发送一个低电平。
1.1.5. 单字节发送
/**
* @brief 传输一个字节数据, 从高位开始
*
* @param data 传输的那一个字节数据
*/
void I2C_Transmit(uint8_t data)
{
/* 按照约定, SCL此时一定为0, 所以可以直接改变SDA电平 */
for (uint8_t i = 0; i < 8; ++i)
{
/* 根据data最高位的值, SDA输出对应的值 */
if ((data & 0x80) != 0)
{
SDA_1;
}
else
{
SDA_0;
}
I2C_DELAY;
/* 拉高SCL, 锁定SDA的值, 等待对方采样SDA */
SCL_1;
I2C_DELAY;
/* 采样结束, 拉低SCL, 进行下一个字节的数据传输 */
SCL_0;
I2C_DELAY;
/* data左移一位, 将下一位移动至最高位 */
data <<= 1;
}
}
1.1.6. 单字节接收
/**
* @brief 接收一个字节数据
*
* @return uint8_t 将接收的数返回
*/
uint8_t I2C_Receive(void)
{
uint8_t recv_data = 0; /* 存储接收的数据 */
/* 释放SDA, 准备采样SDA值 */
SDA_1;
I2C_DELAY;
for (uint8_t i = 0; i < 8; ++i)
{
/* 拉高SCL, 锁定SDA值, 对SDA进行采样 */
SCL_1;
I2C_DELAY;
/* 对SDA进行采样 */
recv_data <<= 1;
if (READ_SDA != 0)
{
recv_data |= 0x01;
}
/* 拉低SCL, 等待接收下一字节数据 */
SCL_0;
I2C_DELAY;
}
/* 返回接收的数据 */
return recv_data;
}
注意:要在接收数据前移动recv_data。若放在接收数据后移动,在最后一个bit接收完后,recv_data还会左移一位,导致接收的数据存储错误。
1.2. EEPROM读写
1.2.1. 获取设备地址
每个从机都有自己的地址,EEPROM作为STM32的从机,也有自己的地址。翻阅产品手册和PCB原理图可知其"读地址"为:0xA1,"写地址"为:0xA0。
EEPROM在使用I2C协议驱动时,需要对其进行初始化:
/**
* @brief 初始化
*
*/
void E2PROM_Init(void)
{
I2C_Init();
}
1.2.2. 单字节写入
写入步骤:
- 发送起始信号;
- 发送待写入设备的地址;
- 指明从设备的哪个地址开始写;
- 写入数据,等待响应;
- 发送结束信号;
- 延时5ms等待EEPROM写入完成。
/**
* @brief 往EEPROM中写入一个字节数据
*
* @param word_address 数据写入的地址
* @param data 要写入的数据
*/
uint8_t E2PROM_WriteByte(uint8_t word_address, uint8_t data)
{
uint8_t status = 0;
/* 1. 发送起始信号 */
I2C_Start();
/* 2. 发送要写入数据的设备的地址 */
I2C_Transmit(DEV_ADDR);
status |= I2C_Wait4Ack();
/* 3. 指明从设备中的哪个地址开始写 */
I2C_Transmit(word_address);
status |= I2C_Wait4Ack();
/* 4. 开始写入数据 */
I2C_Transmit(data);
status |= I2C_Wait4Ack();
/* 5. 发送结束信号 */
I2C_Stop();
/* 6. 等待EEPROM将数据写入 */
Delay_ms(5);
return status;
}
1.2.3. 单字节读取
读取步骤:
- 发送起始信号;
- 发送要读取数据的设备的地址[写],等待响应;
- 指明从设备中的哪个地址开始读,等待响应;
- 再次发送起始信号;
- 发送要读取数据的设备的地址[读],等待响应;
- 接收数据;
- 响应NACK, 停止读取;
- 发送结束信号
/**
* @brief 读取EEPROM中的一个字节数据
*
* @param word_address 要读取数据的地址
* @param p_data 将读到的数据存储在*p_data中
*/
uint8_t E2PROM_ReadByte(uint8_t word_address, uint8_t *p_data)
{
uint8_t status = 0;
/* 1. 发送起始信号 */
I2C_Start();
/* 2. 发送要读取数据的设备的地址[写] */
I2C_Transmit(DEV_ADDR);
status |= I2C_Wait4Ack();
/* 3. 指明从设备中的哪个地址开始读 */
I2C_Transmit(word_address);
status |= I2C_Wait4Ack();
/* 4. 发送起始信号 */
I2C_Start();
/* 5. 发送要读取数据的设备的地址[读] */
I2C_Transmit(DEV_ADDR | 1);
status |= I2C_Wait4Ack();
/* 6. 接收数据 */
*p_data = I2C_Receive();
/* 7. 响应NACK, 停止读取 */
I2C_NAck();
/* 8. 发送结束信号 */
I2C_Stop();
return status;
}
1.2.4. 页写
和单字节写入类似,只是在写入数据时是写入多个字节。
/**
* @brief 往EEPROM中写入多个字节数据
*
* @param word_address 数据写入的起始地址
* @param datas 要写入的数据
* @param size 要写入数据的长度
* @return uint8_t SUCCESS代表写入成功, FAIL代表写入失败
*/
uint8_t E2PROM_WritePage(uint8_t word_address, uint8_t datas[], uint8_t size)
{
uint8_t status = 0;
/* 1. 发送起始信号 */
I2C_Start();
/* 2. 发送要写入数据的设备的地址 */
I2C_Transmit(DEV_ADDR);
status |= I2C_Wait4Ack();
/* 3. 指明从设备中的哪个地址开始写 */
I2C_Transmit(word_address);
status |= I2C_Wait4Ack();
/* 4. 开始写入数据 */
for (uint8_t i = 0; i < size; ++i)
{
I2C_Transmit(datas[i]);
status |= I2C_Wait4Ack();
}
/* 5. 发送结束信号 */
I2C_Stop();
/* 6. 等待EEPROM将数据写入 */
Delay_ms(5);
return status;
}
1.2.5. 页读
和单字节读取类似,只是只在最后一次读取后才返回NAck,其余均返回Ack。
/**
* @brief 读取EEPROM中的多个字节数据
*
* @param word_address 数据读取的起始地址
* @param datas 将读取的数据存储在此
* @param size 要读取数据的长度
* @return uint8_t SUCCESS代表读取成功, FAIL代表读取失败
*/
uint8_t E2PROM_ReadPage(uint8_t word_address, uint8_t datas[], uint8_t size)
{
uint8_t status = 0;
/* 1. 发送起始信号 */
I2C_Start();
/* 2. 发送要读取数据的设备的地址[写] */
I2C_Transmit(DEV_ADDR);
status |= I2C_Wait4Ack();
/* 3. 指明从设备中的哪个地址开始读 */
I2C_Transmit(word_address);
status |= I2C_Wait4Ack();
/* 4. 发送起始信号 */
I2C_Start();
/* 5. 发送要读取数据的设备的地址[读] */
I2C_Transmit(DEV_ADDR | 1);
status |= I2C_Wait4Ack();
/* 6. 接收数据 */
for (uint8_t i = 0; i < size - 1; ++i)
{
datas[i] = I2C_Receive();
I2C_Ack();
}
datas[size - 1] = I2C_Receive();
/* 7. 响应NACK, 停止读取 */
I2C_NAck();
/* 8. 发送结束信号 */
I2C_Stop();
return status;
}
1.2.6. 多页连写
static uint8_t s_record_i = 0; /* 记录多页写下次起始索引 */
/**
* @brief 往EEPROM中写入多个字节数据
*
* @param word_address 数据写入的起始地址
* @param datas 要写入的数据
* @param size 要写入数据的长度
* @return uint8_t SUCCESS代表写入成功, FAIL代表写入失败
*/
uint8_t E2PROM_WritePage(uint8_t word_address, uint8_t datas[], uint8_t size)
{
uint8_t status = 0; /* 写入状态 */
uint8_t low4bit_word = (word_address & 0x0F); /* 记录地址低4位 */
/* 1. 发送起始信号 */
I2C_Start();
/* 2. 发送要写入数据的设备的地址 */
I2C_Transmit(DEV_ADDR);
status |= I2C_Wait4Ack();
/* 3. 指明从设备中的哪个地址开始写 */
I2C_Transmit(word_address);
status |= I2C_Wait4Ack();
/* 4. 开始写入数据 */
uint8_t i;
for (i = 0; i < size; ++i)
{
/* 当前页地址为16时代表此轮不准写入, 应在下一页写 */
if (low4bit_word++ >= 16)
{
s_record_i += i;
break;
}
I2C_Transmit(datas[s_record_i + i]);
status |= I2C_Wait4Ack();
}
/* 5. 发送结束信号 */
I2C_Stop();
/* 6. 等待EEPROM将数据写入 */
Delay_ms(5);
/* 当前页写满, 跳转到下一页继续写 */
if (low4bit_word >= 16)
{
uint8_t high4bit_page = word_address >> 4; /* 记录地址高4位 */
E2PROM_WritePage((high4bit_page + 1) << 4, datas, size - i);
}
s_record_i = 0; /* 清除i记录标记 */
return status;
}
2. 硬件实现I²C
2.1. 硬件图
EEPROM在板子上接的是I2C2,所以,为了使用STM32硬件实现协议,需要开启I²C时钟和对应GPIO寄存器的时钟。
2.2. 初始化
因为要使用到STM32的硬件I²C外设,所以需要开启对应I²C2的时钟;其次,I²C2的SCL和SDA使用的引脚是GPIOB中的PB10和PB11,所以也需要配置GPIOB的时钟。
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; /* 配置GPIOB时钟 */
RCC->APB1ENR |= RCC_APB1ENR_I2C2EN; /* 配置I2C2时钟 */
因为我们使用到了PB10和PB11引脚,所以还需要配置它的工作模式,在芯片手册中已给出如何配置两个引脚工作模式:复用开漏输出。
GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_MODE11);
GPIOB->CRH |= (GPIO_CRH_CNF10 | GPIO_CRH_CNF11);
阅读芯片手册,可知要作为主机使用I²C外设,步骤如下:
第一步:在I2C_CR2寄存器中设定该模块的输入时钟,找到该寄存器说明:
因为I²C2在APB1低速外设总线上,所以FREQ[5:0]最大值只能设置为100100,可以选择合适的速率,这里选择36MHz。
I2C2->CR2 &= ~I2C_CR2_FREQ; /* 先将后5位清零 */
I2C2->CR2 |= (I2C_CR2_FREQ_2 | I2C_CR2_FREQ_5); /* 输入时钟设置为36MHz */
第二步:配置时钟控制寄存器,找到该寄存器说明:
需要设置F/S位为0,使用其标准模式;CCR计算步骤如下图所示,值设置为180。
I2C2->CCR &= ~I2C_CCR_FS; /* 选择标注模式的I²C */
I2C2->CCR &= ~I2C_CCR_CCR; /* 将CCR先清零 */
I2C2->CCR |= 180; /* 配置时钟控制寄存器 */
第三步:配置上升时间寄存器,找到该寄存器的说明:
根据给出的公式:(1000ns / 一个时钟周期) + 1,可算出TRISE的值为37。上升时间就是一个上升沿要多长时间,即从低电平到高电平时间。
I2C2->TRISE = 37; /* 配置上升时间寄存器 */
第四步:编程I2C_CR1寄存器启动外设,查看该寄存器说明:
首先需要设置SMBUS位,设置为I²C模式,再将PE设置为1,使能I²C模块。
I2C2->CR1 &= ~I2C_CR1_SMBUS; /* 设置为I²C模式 */
I2C2->CR1 |= I2C_CR1_PE; /* 使能I²C模块 */
到此为止,所有的初始化完成,完整步骤如下所示:
/**
* @brief 初始化
*
*/
void I2C_Init(void)
{
/* 1. 开启GPIOB、I²C2外设的时钟 */
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
RCC->APB1ENR |= RCC_APB1ENR_I2C2EN;
/* 2. 配置GPIOB引脚工作模式: 复用开漏输出 */
GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_MODE11);
GPIOB->CRH |= (GPIO_CRH_CNF10 | GPIO_CRH_CNF11);
/* 3. 配置I²C2外设 */
I2C2->CR2 &= ~I2C_CR2_FREQ;
I2C2->CR2 |= (I2C_CR2_FREQ_2 | I2C_CR2_FREQ_5); /* 输入时钟设置为36MHz */
I2C2->CCR &= ~(I2C_CCR_FS | I2C_CCR_CCR);
I2C2->CCR |= 180; /* 配置时钟控制寄存器 */
I2C2->TRISE = 37; /* 配置上升时间寄存器 */
I2C2->CR1 &= ~I2C_CR1_SMBUS; /* 使用I²C */
I2C2->CR1 |= I2C_CR1_PE; /* 使能I²C */
}
2.3. 主发送器
以下时是发送数据的流程:
首先需要发送一个起始信号。要发送起始信号只需把I2C_CR1寄存器中的START位置为1即可,其在发出后会由硬件自动清除。
/**
* @brief 发送起始信号
*
*/
void I2C_Start(void)
{
I2C2->CR1 |= I2C_CR1_START; /* 将START位置1 */
}
成功发送起始信号的标志是I2C_SR1寄存器中的SB被置1,此时会触发EV5事件,需要读SR1然后将地址写入DR寄存器清除该事件【SB被清除】,所以这一步可以集成在一个发送设备地址的函数中。清除事件就是将对应位清除,例如这里清除事件EV5就是清除SB。
/**
* @brief 传输地址
*
* @param address 地址值
*/
void I2C_TransmitAddress(uint8_t address)
{
/* 等待起始信号发送完成 */
while ((I2C2->SR1 & I2C_SR1_SB) == 0)
{
}
I2C2->DR = address; /* 此时起始信号以发送完毕, 发送设备地址, 同时清除EV5事件 */
}
在发送完地址后会触发EV6事件【ADDR被置1】,结合上下文,把EV6事件的代码放在传输地址函数里较合理,所以,完整的传输地址函数为:
/**
* @brief 传输地址
*
* @param address 地址值
*/
void I2C_TransmitAddress(uint8_t address)
{
/* 等待起始信号发送完成 */
while ((I2C2->SR1 & I2C_SR1_SB) == 0)
{
}
I2C2->DR = address; /* 此时起始信号以发送完毕, 发送设备地址, 同时清除EV5事件 */
/* 等待地址传输完成 */
while ((I2C2->SR1 & I2C_SR1_ADDR) == 0)
{
}
I2C2->SR2; /* 清除EV6事件【清除ADDR】 */
}
在成功发送地址后,就可发送数据,在发送数据时,需要判断TxE位是否为1,只有在TxE为1时才能发送数据。且写入DR寄存器会将TxE清除,当收到从机响应后,TxE会被硬件置位。这也说明从机响应的Ack会被硬件自动接收判断。
/**
* @brief 传输一个字节数据, 从高位开始
*
* @param data 传输的那一个字节数据
*/
void I2C_Transmit(uint8_t data)
{
/* 等待数据寄存器为空 */
while ((I2C2->SR1 & I2C_SR1_TXE) == 0)
{
}
I2C2->DR = data;
}
若要发多个数据,重复调用I2C_Transmit()函数即可。若不再继续发送,就发送停止信号即可,但在发送结束信号前,需要确保最后一个数据发送完成。
所以,在发生结束信号前,需要判断TxE位和BTF位(数据发送成功时BTF会被置1)是否为1,只有当这两者都为1才表明数据全部发送完成。
/**
* @brief 发送结束信号
*
*/
void I2C_TransmitStop(void)
{
/* 等待最后一个数据发送结束, 再发送停止信号 */
while ((I2C2->SR1 & I2C_SR1_BTF) == 0)
{
}
I2C2->CR1 |= I2C_CR1_STOP;
}
2.4. 主接收器
以下时是接收数据的流程:
首先需要先发送一个起始信号,会触发EV5事件,当起始信号发送成功后,SB位被置1,此时读SR1,然后再将地址写入DR寄存器即可清除该事件【清除SB】。
在发出地址后,会触发EV6事件,当地址发送成功后,ADDR被置1,此时通过读SR1和SR2将清除该事件【清除ADDR】。
之后就可以接收数据,此时会触发EV7事件,当接收到数据时,RxNE会被置1,这时将DR寄存器的内容读取即可清除该事件【清除RxNE】,同时应该响应ACK,将ACK位设置为1。
/**
* @brief 接收一个字节数据
*
* @return uint8_t 将接收的数返回
*/
uint8_t I2C_Receive(void)
{
/* 等待数据寄存器非空 */
while ((I2C2->SR1 & I2C_SR1_RXNE) == 0)
{
}
return I2C2->DR; /* 读取数据, 并且清除EV7事件 */
}
/**
* @brief 响应对方Ack
*
*/
void I2C_Ack(void)
{
I2C2->CR1 |= I2C_CR1_ACK;
}
在接收最后一个数据之前,需要提前将NAck和停止信号发送,也就是说最后一个数据的读取应该在NAck和停止信号后面。这点同样适用于单字节接收,在接收数据前就应该将NAck和停止信号发送。
2.5. I²C协议驱动代码汇总
#ifndef __I2C_H__
#define __I2C_H__
#include "stm32f10x.h"
#include <stdio.h>
/**
* @brief 初始化
*
*/
void I2C_Init(void);
/**
* @brief 发送起始信号
*
*/
void I2C_Start(void);
/**
* @brief 发送时结束信号
*
*/
void I2C_TransmitStop(void);
/**
* @brief 接收时结束信号
*
*/
void I2C_ReceiveStop(void);
/**
* @brief 响应对方Ack
*
*/
void I2C_Ack(void);
/**
* @brief 响应对方NAck
*
*/
void I2C_NAck(void);
/**
* @brief 传输地址
*
* @param address 地址值
*/
void I2C_TransmitAddress(uint8_t address);
/**
* @brief 传输一个字节数据, 从高位开始
*
* @param data 要传输的那一个字节数据
*/
void I2C_Transmit(uint8_t data);
/**
* @brief 接收一个字节数据
*
* @return uint8_t 将接收的数返回
*/
uint8_t I2C_Receive(void);
#endif
#include "i2c.h"
/**
* @brief 初始化
*
*/
void I2C_Init(void)
{
/* 1. 开启GPIOB、I²C2外设的时钟 */
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
RCC->APB1ENR |= RCC_APB1ENR_I2C2EN;
/* 2. 配置GPIOB引脚工作模式: 复用开漏输出 */
GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_MODE11);
GPIOB->CRH |= (GPIO_CRH_CNF10 | GPIO_CRH_CNF11);
/* 3. 配置I²C2外设 */
I2C2->CR2 &= ~I2C_CR2_FREQ;
I2C2->CR2 |= (I2C_CR2_FREQ_2 | I2C_CR2_FREQ_5); /* 输入时钟设置为36MHz */
I2C2->CCR &= ~(I2C_CCR_FS | I2C_CCR_CCR);
I2C2->CCR |= 180; /* 配置时钟控制寄存器 */
I2C2->TRISE = 37; /* 配置上升时间寄存器 */
I2C2->CR1 &= ~I2C_CR1_SMBUS; /* 使用I²C */
I2C2->CR1 |= I2C_CR1_PE; /* 使能I²C */
}
/**
* @brief 发送起始信号
*
*/
void I2C_Start(void)
{
I2C2->CR1 |= I2C_CR1_START; /* 将START位置1 */
}
/**
* @brief 发送结束信号
*
*/
void I2C_TransmitStop(void)
{
/* 等待最后一个数据发送结束, 再发送停止信号 */
while ((I2C2->SR1 & I2C_SR1_BTF) == 0)
{
}
I2C2->CR1 |= I2C_CR1_STOP;
}
/**
* @brief 接收时结束信号
*
*/
void I2C_ReceiveStop(void)
{
I2C2->CR1 |= I2C_CR1_STOP;
}
/**
* @brief 响应对方Ack
*
*/
void I2C_Ack(void)
{
I2C2->CR1 |= I2C_CR1_ACK;
}
/**
* @brief 响应对方NAck
*
*/
void I2C_NAck(void)
{
I2C2->CR1 &= ~I2C_CR1_ACK;
}
/**
* @brief 传输地址
*
* @param address 地址值
*/
void I2C_TransmitAddress(uint8_t address)
{
/* 等待起始信号发送完成 */
while ((I2C2->SR1 & I2C_SR1_SB) == 0)
{
}
I2C2->DR = address; /* 此时起始信号以发送完毕, 发送设备地址 */
/* 等待地址传输完成 */
while ((I2C2->SR1 & I2C_SR1_ADDR) == 0)
{
}
I2C2->SR2; /* 清除EV事件 */
}
/**
* @brief 传输一个字节数据, 从高位开始
*
* @param data 传输的那一个字节数据
*/
void I2C_Transmit(uint8_t data)
{
/* 等待数据寄存器为空 */
while ((I2C2->SR1 & I2C_SR1_TXE) == 0)
{
}
I2C2->DR = data;
}
/**
* @brief 接收一个字节数据
*
* @return uint8_t 将接收的数返回
*/
uint8_t I2C_Receive(void)
{
/* 等待数据寄存器非空 */
while ((I2C2->SR1 & I2C_SR1_RXNE) == 0)
{
}
return I2C2->DR;
}
2.6. EEPROM代码改写
在软件模拟基础上改写即可。
#include "e2prom.h"
/**
* @brief 初始化
*
*/
void E2PROM_Init(void)
{
I2C_Init();
}
/**
* @brief 往EEPROM中写入一个字节数据
*
* @param word_address 数据写入的地址
* @param data 要写入的数据
*/
void E2PROM_WriteByte(uint8_t word_address, uint8_t data)
{
/* 1. 发送起始信号 */
I2C_Start();
/* 2. 发送要写入数据的设备的地址 */
I2C_TransmitAddress(DEV_ADDR);
/* 3. 指明从设备中的哪个地址开始写 */
I2C_Transmit(word_address);
/* 4. 开始写入数据 */
I2C_Transmit(data);
/* 5. 发送结束信号 */
I2C_TransmitStop();
/* 6. 等待EEPROM将数据写入 */
Delay_ms(5);
}
/**
* @brief 读取EEPROM中的一个字节数据
*
* @param word_address 要读取数据的地址
* @param p_data 将读到的数据存储在*p_data中
*/
void E2PROM_ReadByte(uint8_t word_address, uint8_t *p_data)
{
/* 1. 发送起始信号 */
I2C_Start();
/* 2. 发送要读取数据的设备的地址[写] */
I2C_TransmitAddress(DEV_ADDR);
/* 3. 指明从设备中的哪个地址开始读 */
I2C_Transmit(word_address);
/* 4. 发送起始信号 */
I2C_Start();
/* 5. 发送要读取数据的设备的地址[读] */
I2C_TransmitAddress(DEV_ADDR | 1);
/* 6. 响应NACK, 停止读取 */
I2C_NAck();
/* 7. 发送结束信号 */
I2C_ReceiveStop();
/* 8. 接收数据 */
*p_data = I2C_Receive();
}
/**
* @brief 往EEPROM中写入多个字节数据
*
* @param word_address 数据写入的起始地址
* @param datas 要写入的数据
* @param size 要写入数据的长度
*/
void E2PROM_WritePage(uint8_t word_address, uint8_t datas[], uint8_t size)
{
/* 1. 发送起始信号 */
I2C_Start();
/* 2. 发送要写入数据的设备的地址 */
I2C_TransmitAddress(DEV_ADDR);
/* 3. 指明从设备中的哪个地址开始写 */
I2C_Transmit(word_address);
/* 4. 开始写入数据 */
for (uint8_t i = 0; i < size; ++i)
{
I2C_Transmit(datas[i]);
}
/* 5. 发送结束信号 */
I2C_TransmitStop();
/* 6. 等待EEPROM将数据写入 */
Delay_ms(5);
}
/**
* @brief 读取EEPROM中的多个字节数据
*
* @param word_address 数据读取的起始地址
* @param datas 将读取的数据存储在此
* @param size 要读取数据的长度
*/
void E2PROM_ReadPage(uint8_t word_address, uint8_t datas[], uint8_t size)
{
/* 1. 发送起始信号 */
I2C_Start();
/* 2. 发送要读取数据的设备的地址[写] */
I2C_TransmitAddress(DEV_ADDR);
/* 3. 指明从设备中的哪个地址开始读 */
I2C_Transmit(word_address);
/* 4. 发送起始信号 */
I2C_Start();
/* 5. 发送要读取数据的设备的地址[读] */
I2C_TransmitAddress(DEV_ADDR | 1);
/* 6. 接收数据 */
for (uint8_t i = 0; i < size - 1; ++i)
{
I2C_Ack();
datas[i] = I2C_Receive();
}
/* 7. 响应NACK, 停止读取 */
I2C_NAck();
/* 8. 发送结束信号 */
I2C_ReceiveStop();
datas[size - 1] = I2C_Receive();
}
为什么响应ACK和NAck都要在接收数据前呢?