目录
I²C的物理层
I²C的协议层
I²C特点
I²C 总线时序图
软件模拟I²C时序分享
例程简介
例程分享
STM32的I²C外设
IIC(Inter-Integrated Circuit),也称为I²C或TWI(Two-Wire Interface),是一种广泛使用的串行总线接口,用于连接低速度的集成电路。这种通信协议非常适合在单个主设备和多个从设备之间进行短距离通信。
IIC通信只需要两根线:一个是串行数据线(SDA),另一个是串行时钟线(SCL)。这两根线都需要通过上拉电阻连接到正电源,以确保在没有信号驱动时,线路能够保持在高电平状态。
IIC协议定义了一系列的信号,包括开始信号、停止信号、数据有效性和应答信号。开始信号和停止信号用于标识一次通信的开始和结束,而数据有效性确保数据在时钟信号稳定时被读取。应答信号则是从设备对接收数据的确认。
I2C由于其简单和灵活的特性,成为了连接低速外围设备,如传感器、EEPROM、显示器等的理想选择。
总线时序图是理解IIC通信的关键。它展示了开始信号、数据位的传输、应答位以及停止信号的顺序。在IIC通信中,数据位在SCL线为高电平时被认为是稳定的,因此数据应该在SCL的高电平期间被读取。
起始条件: SCL高电平期间,SDA从高电平切换到低电平
终止条件: SCL高电平期间,SDA从低电平切换到高电平
起始和终止条件都是由主机产生
发送一个字节: SCL低电平期间,主机将数据位依次放到SDA线上,(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许由数据变化,依次循环上述过程8次即可发送一个字节
接收一个字节: SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所哟一SCL高电平期间SDA不允许有数据变换,依次循环上述过程8次,即可接收一个字节(主机在接收数据前需要先释放SDA)
发送应答: 主机在接收完一个字节后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答
接收应答: 主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前需要释放SDA)
/**
* @brief 定义SCL写函数
* @param None
* @retval None
*/
void myi2c_w_scl(uint8_t bitval){
GPIO_WriteBit(GPIOA, GPIO_Pin_1, (BitAction)bitval); //将bitval的值写入GPIOA的Pin_1,也就是SCL线
Delay_us(10); //延迟10微秒
}
/**
* @brief 定义SDA写函数
* @param None
* @retval None
*/
void myi2c_w_sda(uint8_t bitval){
GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)bitval); //将bitval的值写入GPIOA的Pin_0,也就是SDA线
Delay_us(10); //延迟10微秒
}
/**
* @brief 读取SDA数据
* @param None
* @retval None
*/
uint8_t myi2c_r_sda(void){
return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0); //读取GPIOA的Pin_0,也就是SDA线的值
}
/**
* @brief 软件模拟I2C初始化
* SDA PA0 推挽输出
* SCL PA1 推挽输出
* @param None
* @retval None
*/
void myi2c_init(void){
//初始化GPIO口
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能GPIOA时钟
GPIO_InitTypeDef GPIO_InitStructure; //定义GPIO初始化结构体
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; //设置GPIO模式为开漏输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; //设置GPIO的Pin_0和Pin_1
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //设置GPIO速度为50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化GPIOA
//释放总线
GPIO_SetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1); //将GPIOA的Pin_0和Pin_1设置为高电平,释放总线
}
/**
* @brief I2C起始条件
* @param None
* @retval None
*/
void i2c_start(void){
//输出起始条件
myi2c_w_sda(1); //将SDA线设置为高电平
myi2c_w_scl(1); //将SCL线设置为高电平
myi2c_w_sda(0); //将SDA线设置为低电平,生成起始条件
myi2c_w_scl(0); //将SCL线设置为低电平
}
/**
* @brief I2C结束条件
* @param None
* @retval None
*/
void i2c_stop(void){
//输出起始条件
myi2c_w_sda(0); //将SDA线设置为低电平
myi2c_w_scl(1); //将SCL线设置为高电平
myi2c_w_sda(1); //将SDA线设置为高电平,生成结束条件
}
/**
* @brief I2C发送一个字节
* @param None
* @retval None
*/
void myi2c_sendbyte(uint8_t byte){
for(uint8_t i = 0; i < 8; i++){ //循环8次,发送一个字节
myi2c_w_sda(byte & 0x80 >> i); //每发送一次向右偏移一个字节
myi2c_w_scl(1); //将SCL线设置为高电平
myi2c_w_scl(0); //将SCL线设置为低电平
}
}
/**
* @brief I2C接收一个字节
* @param None
* @retval None
*/
uint8_t myi2c_recv_byte(void){
uint8_t byte = 0; //定义一个字节变量
for(uint8_t i = 0; i < 8; i++){ //循环8次,接收一个字节
myi2c_w_scl(1); //将SCL线设置为高电平
if(myi2c_r_sda() == 1){byte |= (0x80 >> i);} //如果SDA线为高电平,将byte的相应位设置为1
myi2c_w_scl(0); //将SCL线设置为低电平
}
return byte; //返回接收到的字节
}
/**
* @brief I2C接收应答
* @param None
* @retval None
*/
uint8_t myi2c_recv_ack(void){
uint8_t ackbit = 0; //定义一个应答位变量
myi2c_w_sda(1); //将SDA线设置为高电平
myi2c_w_scl(1); //将SCL线设置为高电平
ackbit = myi2c_r_sda(); //读取SDA线的值,也就是应答位
myi2c_w_scl(0); //将SCL线设置为低电平
return ackbit; //返回应答位
}
/**
* @brief I2C发送应答
* @param None
* @retval None
*/
void myi2c_send_ack(uint8_t ackbit){
myi2c_w_sda(ackbit); //将应答位的值写入SDA线
myi2c_w_scl(1); //将SCL线设置为高电平
myi2c_w_scl(0); //将SCL线设置为低电平
}
软件模拟IIC驱动AT24C02分享
通过I2C协议与AT24C04 EEPROM芯片进行交互的函数。EEPROM代表电可擦除可编程只读存储器,这是一种非易失性存储器,用于计算机和其他电子设备中存储断电后必须保存的少量数据。
以下是每个函数的简要概述:
AT24_init
:此函数初始化与AT24C04芯片通信的I2C接口。AT24_write_byte
:此函数将单个字节的数据写入AT24C04芯片的指定地址。AT24_read_byte
:此函数从AT24C04芯片的指定地址读取单个字节的数据。AT24_write_page
:此函数将多个字节的数据写入AT24C04芯片的指定地址。AT24C04的内存被划分为多个页面,每个页面可以容纳多个字节的数据。AT24_WriteBuffer
:此函数将数据缓冲区写入AT24C04芯片。它考虑到芯片内存的页面结构,并在必要时跨多个页面写入数据。AT24_readBuffer
:此函数从AT24C04芯片读取数据缓冲区。与AT24_WriteBuffer
一样,它也考虑到芯片内存的页面结构。/*源代码*/
#include "AT24.h"
uint8_t AT24_ADDR_W1 = 0XA0;
uint8_t AT24_ADDR_W2 = 0XA2;
uint8_t AT24_ADDR_R1 = 0xA1;
uint8_t AT24_ADDR_R2 = 0xA3;
/**
* @brief AT24C04初始化
* @param None
* @retval None
*/
void AT24_init(void){
myi2c_init();
}
/**
* @brief 指定地址写入一个字节数据(0 ---- 255)
* @param uint16_t addr 写入数据地址
* @param uint8_t data 写入字节
* @retval 写入成功返回4
*/
uint8_t AT24_write_byte(uint16_t addr, uint8_t data){
i2c_start(); //发送起始信号
myi2c_sendbyte(AT24_ADDR_W1); //发送从机地址
if(myi2c_recv_ack() == 1){
i2c_stop(); //发送停止位
printf("AT24寻址未应答\r\n");
return 1;
}
myi2c_sendbyte(addr); //发送要写入的地址
if(myi2c_recv_ack() == 1){
i2c_stop(); //发送停止位
printf("AT24内部寻址未应答\r\n");
return 2;
}
myi2c_sendbyte(data); //发送要写入的数据
if(myi2c_recv_ack() == 1){
i2c_stop(); //发送停止位
printf("AT24写入数据未应答\r\n");
return 3;
}
i2c_stop(); //发送停止位
printf("AT24写入数据成功\r\n");
return 4;
}
/**
* @brief 指定地址读出一个字节数据(0 ---- 255)
* @param uint16_t addr 读数据地址
* @retval 成功返回读出数据
*/
uint8_t AT24_read_byte(uint16_t addr){
uint8_t read_data = 0;
i2c_start(); //发送起始信号
myi2c_sendbyte(AT24_ADDR_W1); //发送从机地址
if(myi2c_recv_ack() == 1){
i2c_stop(); //发送停止位
printf("AT24寻址未应答\r\n");
return 1;
}
myi2c_sendbyte(addr); //发送要写入的地址
if(myi2c_recv_ack() == 1){
i2c_stop(); //发送停止位
printf("AT24内部寻址未应答\r\n");
return 2;
}
i2c_stop(); //发送停止位
i2c_start(); //发送起始信号
myi2c_sendbyte(AT24_ADDR_R1); //发送从机地址
if(myi2c_recv_ack() == 1){
i2c_stop(); //发送停止位
printf("AT24寻址未应答\r\n");
return 1;
}
read_data = myi2c_recv_byte();
myi2c_send_ack(1);
i2c_stop(); //发送停止位
return read_data;
}
/**
* @brief 指定地址页写入数据(0 ---- 255)
* @param uint16_t addr 写入数据地址
* @param uint8_t data 写入字节首地址
* @param uint8_t num 写入字节个数
* @retval 写入成功返回4
*/
uint8_t AT24_write_page(uint16_t addr, uint8_t num, uint8_t *data){
i2c_start(); //发送起始信号
myi2c_sendbyte(AT24_ADDR_W1); //发送从机地址
if(myi2c_recv_ack() == 1){
i2c_stop(); //发送停止位
printf("AT24寻址未应答\r\n");
return 1;
}
myi2c_sendbyte(addr); //发送要写入的地址
if(myi2c_recv_ack() == 1){
i2c_stop(); //发送停止位
printf("AT24内部寻址未应答\r\n");
return 2;
}
while(num--){
myi2c_sendbyte(*data); //发送要写入的数据
if(myi2c_recv_ack() == 1){
i2c_stop(); //发送停止位
printf("AT24写入数据未应答\r\n");
return 3;
}
data++;
}
i2c_stop(); //发送停止位
printf("AT24写入数据成功\r\n");
return 4;
}
/**
* @brief 随机写
* @param uint8_t *pBuffer 写入数据的首地址
* @param uint32_t WriteAddr 写入地址
* @param uint16_t NumByteToWrite 数据长度
* @retval None
*/
void AT24_WriteBuffer(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite){
uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;
Addr = WriteAddr % 16; //判断地址是否为整页
count = 16 - Addr; //当前页剩余字节数
NumOfPage = NumByteToWrite / 16; //需要的整页数
NumOfSingle = NumByteToWrite % 16; //除整页剩余的字节数
if (Addr == 0) /*整页开始 */
{
if (NumOfPage == 0) /*所写数据不够一整页,直接调用页编程函数 */
{
AT24_write_page(WriteAddr, NumByteToWrite, pBuffer);
}
else /*所写数据超过一页*/
{
while (NumOfPage--) //整页写
{
AT24_write_page(WriteAddr, 16, pBuffer);
WriteAddr += 16;
pBuffer += 16;
}
AT24_write_page(WriteAddr, NumOfSingle, pBuffer); //除整页之外剩余的
}
}
else /*不是整页开始写 */
{
if (NumOfPage == 0) /*所写不到一页 */
{
if (NumOfSingle > count) /*所需空间大于当前页所剩空间*/
{
temp = NumOfSingle - count; //当前页写完之后剩余量
AT24_write_page(WriteAddr, count, pBuffer); //在当前页写,写满
WriteAddr += count;
pBuffer += count;
AT24_write_page(WriteAddr, temp, pBuffer); //剩余写入下一页
}
else
{
AT24_write_page(WriteAddr, NumByteToWrite, pBuffer); //直接写当前页
}
}
else /*写入数据量大于一页 */
{
NumByteToWrite -= count; //写满当前页所剩数据
NumOfPage = NumByteToWrite / 16; //要写入的整页
NumOfSingle = NumByteToWrite % 16; //写完整页剩余的字节
AT24_write_page(WriteAddr, count, pBuffer);//把当前页写满
WriteAddr += count;
pBuffer += count;
while (NumOfPage--) //写整页
{
AT24_write_page(WriteAddr, 16, pBuffer);
WriteAddr += 16;
pBuffer += 16;
}
if (NumOfSingle != 0) //写剩余不满一页的字节
{
AT24_write_page(WriteAddr, NumOfSingle, pBuffer);
}
}
}
}
/**
* @brief 随便读
* @param None
* @retval None
*/
uint8_t AT24_readBuffer(uint16_t addr, uint16_t num, uint8_t *recvdata){
i2c_start(); //发送起始信号
myi2c_sendbyte(AT24_ADDR_W1); //发送从机地址
Delay_us(10);
if(myi2c_recv_ack() == 1){
i2c_stop(); //发送停止位
printf("AT24器件寻址未应答\r\n");
return 1;
}
myi2c_sendbyte(addr); //发送要写入的地址
if(myi2c_recv_ack() == 1){
i2c_stop(); //发送停止位
printf("AT24内部寻址未应答\r\n");
return 2;
}
i2c_stop(); //发送停止位
i2c_start(); //发送起始信号
myi2c_sendbyte(AT24_ADDR_R1); //发送从机地址
if(myi2c_recv_ack() == 1){
i2c_stop(); //发送停止位
printf("AT24器件2寻址未应答\r\n");
return 1;
}
while(num--){
*recvdata = myi2c_recv_byte();
myi2c_send_ack(0);
recvdata++;
Delay_us(5);
}
myi2c_send_ack(1);
i2c_stop(); //发送停止位
return num;
}
/*头文件*/
#ifndef __AT24_H_
#define __AT24_H_
#include "stm32f4xx.h" // Device header
#include "myi2c.h"
#include "usart.h"
#include "delay.h"
void AT24_init(void);
uint8_t AT24_write_byte(uint16_t addr, uint8_t data);
uint8_t AT24_read_byte(uint16_t addr);
uint8_t AT24_write_page(uint16_t addr, uint8_t num, uint8_t *data);
void AT24_WriteBuffer(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite);
uint8_t AT24_readBuffer(uint16_t addr, uint16_t num, uint8_t *recvdata);
#endif
STM32内部集成了硬件I²C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据发送等功能,减轻CPU的负担。STM32的I²C外设支持多主机模式、7位或10位地址模式、不同的通信速度(标准速度高达100KHZ,快速400KHZ)、DMA,以及兼容SMBus协议。
这些特点使得STM32的硬件I2C模块成为在嵌入式系统中实现I2C通信的理想选择,提供了方便、高效和可靠的通信功能。