当前位置: 首页 > news >正文

绝对让你明明白白,脚把脚带你盯着 I2C 时序图将 I2C 程序给扣出来(基于STM32的模拟I2C)

目录

  • 前言
  • 一、关于STM32 I/O端口位的基本结构讲解
  • 二、模拟I2C编写前的需知道的知识
    • 1、I2C简介
    • 2、根据时序编写模拟I2C程序重要的两点
      • Ⅰ、主机发送数据给从机时的时序控制
      • Ⅱ、主机接收来自从机的数据时的时序控制
      • Ⅲ、完整的I2C时序图(按写程序的思想分割时序,每一个对应一个功能)
  • 三、模拟I2C分解时序编写程序
    • 1、起始信号
    • 2、一个字节即8bit数据发送
    • 3、释放SDA,等待接收从机应答信号
    • 4、读取从机的一个字节8bit数据
    • 5、发送应答/非应答信号
    • 6、停止信号
  • 四、读取 AT24C02 EEPROM驱动程序(下面程序很长,不需要可以跳过)
    • 1、iic.h文件
    • 2、iic.c文件
    • 3、24cxx.h文件
    • 4、24cxx.c文件

前言

  这一篇文章是我大学时候写了一半的草稿,为了不让其荒废,我重新梳理完善,让大家都能明白IIC通信的原理及编写出其通信协议的程序。我相信很多同学学I2C的时候基本只是单纯看别人的文章或者是看人家人家的视频,然后做实验只是照着人家的程序敲了一遍,拿着人家的程序和I2C时序(好像懂了),真正自己写就并非能写得出来了。
  这其实是大家对MCU的IO输出和I2C时序没有理解透,说真的,MCU里面最常用的UART、I2C、SPI这三个传输协议里面,I2C协议是最简单的,为什么这么说呢,初学者会认为I2C通信要写设备地址、又要写寄存器地址,还整什么应答的,哪有UART简单(只有起始位、数据位、校验位、停止位)。那我和你说说,UART的TX线是我方主动发送,确实不用注意什么,但是接收呢?我方只能老老实实遵循双方协定的波特率去接收它方发送过来的数据,也就是说时钟要精准,并且要根据时钟精准拿捏每一位的数据,这样读取出来的数据才无误,大家可以看看我之前写的模拟串口文章,而I2C的时钟是由我方主机端控制的,也就是说时钟频率只要在它方允许范围内,我们是能正常通信的,这为什么这么说呢?时钟其实就是脉冲信号,而每一个脉冲就是让它方从机每走一步的动力,因此在允许范围呢,它方从机被我方死死拿捏了。
  废话不多说,开讲…

一、关于STM32 I/O端口位的基本结构讲解

  其实这个我在之前就发表过STM32与IO控制相关的文章了,大家也可阅读“STM32IO文章”。
  下图是我在STM32中文参考手册截取出来的。
在这里插入图片描述
  这里我讲一下为什么我们在写模拟I2C的时候需要将SDA线上的IO配置为开漏输出。上图中的上P-MOS管和下N-MOS管组合成了推挽输出电路,电子工程师一般叫图腾柱输出电路,这个电路优点在于输出响应快,而且驱动能力强。其工作原理就是设置输出高电平,那么上部分的P-MOS的G极低电平(Vgs<0),P-MOS被激活,因此P-MOS的S、D极导通,向外推出VDD电平,而下部分正好与上部分相反,N-MOS管处于非激活状态;当设置输出低电平时,那么下部分的N-MOS的G极高电平(Vgs>0),N-MOS被激活,因此N-MOS的S、D极导通,将外部电平值拉下低电平。(提示:上下两管公用极是漏极D)
  好,经过推挽电路的讲解大家已经知道其工作原理了,而开漏输出就是只使用了下管电路,也就是上图红色标线的线路,当G极为低电平是,N-MOS处于非激活状态,也就是S、D极不导通,那么电路处于高阻太,I2C的SDA线就呈现只有上拉电阻拉高的高电平状态,也就是说此时SDA电平信号是不受我方主机控制的,从机可以随意控制高、低电平。在STM32中文参考手册有一段话,如下图所示:
在这里插入图片描述
  也就是说,我们设置开漏模式是可以读取IO的输入电平的,因此我们要读取SDA线上从机的信号,务必要将SDA输出的IO为高电平,才能有效读取从机输入的电平信号。

二、模拟I2C编写前的需知道的知识

1、I2C简介

这里我就使用正点原子的开发指南贴给大家看啦,当个搬运工。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2、根据时序编写模拟I2C程序重要的两点

  大家看了I2C简介后应该对其有大致了解了吧,能影响、控制I2C通讯的有三个通讯状态:1、SCL低电平,数据传输无效; 2、SDA下降沿信号触发I2C起始信号; 3、SDA上升沿信号触发I2C停止信号。也就是说I2C通讯中途一定要避免这后面两个事件发送。

Ⅰ、主机发送数据给从机时的时序控制

在这里插入图片描述
  上部分的时序是时钟SCL,下部分的是数据SDA。细心观察的你们肯定发现了,发送数据的电平占用了前1//4周期的低电平时钟 和 后1/4周期的低电平时钟,这是为什么呢,因为我们在1/2周期的高电平时钟期间内才设置发送数据位的电平信号,那么很可能会触发前面说的起始信号和停止信号事件,这是我们不期望的,所以我们可以利用前后1/4周期的低电平时钟这个无效数据传输期间把发送的数据位的电平信号提前和延后来避免。

Ⅱ、主机接收来自从机的数据时的时序控制

在这里插入图片描述
  前面1/2时钟周期我们需要释放SDA,不过前提是我们主机持有SDA才需要释放,比如主机发送完8bit数据后需要接收从机的应答信号,因此需要要先释放SDA,然后等待接收从机应答信号;如果是主机接收完来自从机的8bit数据后是不需要释放SDA的,因为这个期间是从机持有SDA,而不是主机持有SDA,因此接收完数据后的主机应该持回SDA,发送应答信号给从机,如果主机继续接收下一个字节数据,那么就需要先释放SDA,再去接收数据了。
前面也知道,释放SDA(I2C总线空闲)需要SDA和SCL都为高电平。这里我们提前在SCL为低电平无效数据传输阶段先把SDA拉到高电平,前面也说过了,反正上、下边沿信号触发起始、停止信号,而且其还有一个妙处就是在SCL高电平时,并没有立即读取SDA电平信号,而是延迟了1/4个时钟周期,这里巧妙在立即读取数据时,I2C的SDA可能还没有释放完毕,还有就算SDA释放完了,可能从机还没有将它要发送的应答信号或者是发送数据的第一个bit信号还未发送出来,那我方主机就可能读取的信号不正确了,延迟可以确保读取的数据准确性。
  经过上面分析,也就是无论是主机发送数据后接收应答信号,还是主机连接接收从机数据,都需要先释放SDA。这里我们也可以用实际IIC通讯过程图分析,如下图所示:
在这里插入图片描述
  写操作没什么好说的,就是主机持有SDA发送数据,然后释放SDA,读取从机发送的数据。而读操作大家应该也注意到了我用①②标注的红色框框的读一个字节的操作过程吧,大家会说明明主机在发送从机地址与读写操作后,已经在等待接收从机应答前释放了SDA了,而①在读的时候又重复释放了SDA,会不会有问题呀。答案是没问题的,因为在前面就说过我们SDA是使用开漏输出的,释放SDA就是让SDA线处于高电平,也就是说此时MCU这边是没有IO与SDA线呈现高阻态(可认为就是断开的),因此重复断开是完全不影响数据的读取结果的(都断开了,再断现在是没有改变的),除了这个原因外,①的重复释放SDA,是为了保证模拟时序的完整性。

Ⅲ、完整的I2C时序图(按写程序的思想分割时序,每一个对应一个功能)

在这里插入图片描述
  细心的你们应该发现了一点,除了其实信号和停止信号的时序只有3/4个时钟周期的,而接收bit位数据信号和发送bit位数据的时序都是一个周期的(前1/4低电平时钟 + 中1/2高电平时钟 + 后1/4低电平时钟),也就是说这样编写出来的I2C通讯时序是绝对完整的。

三、模拟I2C分解时序编写程序

时序图中,一个时钟分为四段,每一段时长是2us,也就是一个时钟周期是8us,频率为125KHz,那么传输速度就是125K bit/s。

1、起始信号

在这里插入图片描述

/* 产生IIC起始信号 */
void iic_start(void)
{	/* 空闲状态 */IIC_SCL(1);IIC_SDA(1);iic_delay();		//延时2us/* START信号: 当SCL为高时, SDA从高变成低, 表示起始信号 *//* 下降沿信号,iic开始信号 */IIC_SCL(1);IIC_SDA(0);     iic_delay();/* 时钟低电平,数据传输无效,持有SDA,钳住数据 */IIC_SCL(0);     IIC_SDA(0); iic_delay();
}

2、一个字节即8bit数据发送

在这里插入图片描述

/* IIC发送一个字节* data: 要发送的数据*/
void iic_send_byte(uint8_t data)
{uint8_t t;/* SDA数据电平状态保持在 前1/4低电平时钟 + 1/2高电平时钟 + 后1/4时钟,确保在1/2高电平时钟期间没有SDA的边沿跳变信号*/for (t = 0; t < 8; t++){	/* 继上一个低电平时钟无效数据前一半 */IIC_SCL(0);IIC_SDA((data & 0x80) >> 7);    /* 高位先发送 */iic_delay();IIC_SCL(1);IIC_SDA((data & 0x80) >> 7);    /* 高位先发送 */iic_delay();iic_delay();IIC_SCL(0);IIC_SDA((data & 0x80) >> 7);    /* 高位先发送 */iic_delay();data <<= 1;     /* 左移1位,用于下一次发送 */}
}

3、释放SDA,等待接收从机应答信号

在这里插入图片描述

/* 等待应答信号到来* 返回值1,接收应答失败*       0,接收应答成功*/
uint8_t iic_wait_ack(void)
{uint8_t waittime = 0;uint8_t rack = 0;/* 主机释放SDA线(此时外部器件可以拉低SDA线) */IIC_SCL(0);		//继主机发送一字节后的前低电平1/4时钟IIC_SDA(1);    iic_delay();IIC_SCL(1);     /* SCL=1, 此时从机可以返回ACK */IIC_SDA(1); 	//继续释放SDA线,为读取从机的应答信号iic_delay();while (IIC_READ_SDA)    /* 等待应答 */{waittime++;if (waittime > 250){iic_stop();rack = 1;break;}}IIC_SCL(0);     /* SCL=0, 结束ACK检查 */iic_delay();return rack;
}

4、读取从机的一个字节8bit数据

在这里插入图片描述

/* IIC读取一个字节* 返回接收到的数据*/
uint8_t iic_read_byte(void)
{uint8_t i, receive = 0;for (i = 0; i < 8; i++ )    /* 接收1个字节数据 */{receive <<= 1;  /* 高位先输出,所以先收到的数据位要左移 */IIC_SCL(0);IIC_SDA(1);iic_delay();/* 先输出高电平时钟,并且延时1/4时钟,确保读取的是正确的电平信号,如果不延时的话,我们立即读取的电平信号可能是从机还未设置输出对应的数据电平 *//* 而SDA设置高电平,那么SCL和SDA都为高,也就是把主机SDA释放,给从机持有SDA发送数据,那么主机接收从机发送的数据 */IIC_SCL(1);IIC_SDA(1);iic_delay();if (IIC_READ_SDA){receive++;}iic_delay();IIC_SCL(0);iic_delay();}return receive;
}

5、发送应答/非应答信号

在这里插入图片描述

/* 产生ACK应答 */
void iic_ack(void)
{IIC_SCL(0);     /* 产生一个时钟 */IIC_SDA(0);     /* SCL 0 -> 1  时 SDA = 0,表示应答 */iic_delay();/* 应答信号 */IIC_SCL(1);     /* 产生一个时钟 */IIC_SDA(0);iic_delay();iic_delay();IIC_SCL(0);IIC_SDA(0); iic_delay();
}/* 不产生ACK应答 */
void iic_nack(void)
{IIC_SCL(0);     /* 产生一个时钟 */IIC_SDA(1);     /* SCL 0 -> 1  时 SDA = 0,表示应答 */iic_delay();/* 非应答信号 */IIC_SCL(1);     /* 产生一个时钟 */IIC_SDA(1);iic_delay();iic_delay();IIC_SCL(0);IIC_SDA(1); iic_delay();
}

6、停止信号

/* 产生IIC停止信号 */
void iic_stop(void)
{/* STOP信号: 当SCL为高时, SDA从低变成高, 表示停止信号 */IIC_SCL(0);IIC_SDA(0);     iic_delay();/* 时钟有效 */IIC_SCL(1);IIC_SDA(0);iic_delay();/* 上升沿信号,iic停止信号 */IIC_SCL(1);IIC_SDA(1);     /* 发送I2C总线结束信号 */iic_delay();
}

四、读取 AT24C02 EEPROM驱动程序(下面程序很长,不需要可以跳过)

1、iic.h文件

#ifndef __IIC_H
#define __IIC_H#include "main.h"/******************************************************************************************/
/* 引脚 定义 */#define IIC_SCL_GPIO_PORT               GPIOH
#define IIC_SCL_GPIO_PIN                GPIO_PIN_4
#define IIC_SCL_GPIO_CLK_ENABLE()       do{ __HAL_RCC_GPIOH_CLK_ENABLE(); }while(0)   /* PB口时钟使能 */#define IIC_SDA_GPIO_PORT               GPIOH
#define IIC_SDA_GPIO_PIN                GPIO_PIN_5
#define IIC_SDA_GPIO_CLK_ENABLE()       do{ __HAL_RCC_GPIOH_CLK_ENABLE(); }while(0)   /* PB口时钟使能 *//******************************************************************************************//* IO操作 */
#define IIC_SCL(x)        do{ x ? \HAL_GPIO_WritePin(IIC_SCL_GPIO_PORT, IIC_SCL_GPIO_PIN, GPIO_PIN_SET) : \HAL_GPIO_WritePin(IIC_SCL_GPIO_PORT, IIC_SCL_GPIO_PIN, GPIO_PIN_RESET); \}while(0)       /* SCL */#define IIC_SDA(x)        do{ x ? \HAL_GPIO_WritePin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN, GPIO_PIN_SET) : \HAL_GPIO_WritePin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN, GPIO_PIN_RESET); \}while(0)       /* SDA */#define IIC_READ_SDA     HAL_GPIO_ReadPin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN)      /* 读取SDA *//* IIC所有操作函数 */
void iic_init(void);            /* 初始化IIC的IO口 */
void iic_start(void);           /* 发送IIC开始信号 */
void iic_stop(void);            /* 发送IIC停止信号 */
void iic_ack(void);             /* IIC发送ACK信号 */
void iic_nack(void);            /* IIC不发送ACK信号 */
uint8_t iic_wait_ack(void);     /* IIC等待ACK信号 */
void iic_send_byte(uint8_t txd);/* IIC发送一个字节 */
uint8_t iic_read_byte(void);/* IIC读取一个字节 */#endif

2、iic.c文件

#include "iic.h"/* 初始化IIC */
void iic_init(void)
{GPIO_InitTypeDef gpio_init_struct;IIC_SCL_GPIO_CLK_ENABLE();  /* SCL引脚时钟使能 */IIC_SDA_GPIO_CLK_ENABLE();  /* SDA引脚时钟使能 */gpio_init_struct.Pin = IIC_SCL_GPIO_PIN;gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;        /* 推挽输出 */gpio_init_struct.Pull = GPIO_PULLUP;                /* 上拉 */gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; /* 快速 */HAL_GPIO_Init(IIC_SCL_GPIO_PORT, &gpio_init_struct);/* SCL */gpio_init_struct.Pin = IIC_SDA_GPIO_PIN;gpio_init_struct.Mode = GPIO_MODE_OUTPUT_OD;        /* 开漏输出 */HAL_GPIO_Init(IIC_SDA_GPIO_PORT, &gpio_init_struct);/* SDA *//* SDA引脚模式设置,开漏输出,上拉, 这样就不用再设置IO方向了, 开漏输出的时候(=1), 也可以读取外部信号的高低电平 */iic_stop();     /* 停止总线上所有设备 */
}/* IIC延时函数,用于控制IIC读写速度,这是定义1/4个时钟时长 */
static void iic_delay(void)
{delay_us(2);    /* 2us的延时, 读写速度在250Khz以内 */
}/* 产生IIC起始信号 */
void iic_start(void)
{	/* 空闲状态 */IIC_SCL(1);IIC_SDA(1);iic_delay();/* START信号: 当SCL为高时, SDA从高变成低, 表示起始信号 *//* 下降沿信号,iic开始信号 */IIC_SCL(1);IIC_SDA(0);     iic_delay();/* 时钟低电平,数据传输无效,持有SDA,钳住数据 */IIC_SCL(0);     IIC_SDA(0); iic_delay();
}/* 产生IIC停止信号 */
void iic_stop(void)
{/* STOP信号: 当SCL为高时, SDA从低变成高, 表示停止信号 */IIC_SCL(0);IIC_SDA(0);     iic_delay();/* 时钟有效 */IIC_SCL(1);IIC_SDA(0);iic_delay();/* 上升沿信号,iic停止信号 */IIC_SCL(1);IIC_SDA(1);     /* 发送I2C总线结束信号 */iic_delay();
}/* 等待应答信号到来* 返回值1,接收应答失败*       0,接收应答成功*/
uint8_t iic_wait_ack(void)
{uint8_t waittime = 0;uint8_t rack = 0;/* 主机释放SDA线(此时外部器件可以拉低SDA线) */IIC_SCL(0);		//继主机发送一字节后的前低电平1/4时钟IIC_SDA(1);    iic_delay();IIC_SCL(1);     /* SCL=1, 此时从机可以返回ACK */IIC_SDA(1); 	//继续释放SDA线,为读取从机的应答信号iic_delay();while (IIC_READ_SDA)    /* 等待应答 */{waittime++;if (waittime > 250){iic_stop();rack = 1;break;}}IIC_SCL(0);     /* SCL=0, 结束ACK检查 */iic_delay();return rack;
}/* 产生ACK应答 */
void iic_ack(void)
{IIC_SCL(0);     /* 产生一个时钟 */IIC_SDA(0);     /* SCL 0 -> 1  时 SDA = 0,表示应答 */iic_delay();/* 应答信号 */IIC_SCL(1);     /* 产生一个时钟 */IIC_SDA(0);iic_delay();iic_delay();IIC_SCL(0);IIC_SDA(0);iic_delay();
}/* 不产生ACK应答 */
void iic_nack(void)
{IIC_SCL(0);     /* 产生一个时钟 */IIC_SDA(1);     /* SCL 0 -> 1  时 SDA = 0,表示应答 */iic_delay();/* 非应答信号 */IIC_SCL(1);     /* 产生一个时钟 */IIC_SDA(1);iic_delay();iic_delay();IIC_SCL(0);IIC_SDA(1);iic_delay();
}/* IIC发送一个字节* data: 要发送的数据*/
void iic_send_byte(uint8_t data)
{uint8_t t;/* SDA数据电平状态保持在 前1/4低电平时钟 + 1/2高电平时钟 + 后1/4时钟,确保在1/2高电平时钟期间没有SDA的边沿跳变信号*/for (t = 0; t < 8; t++){	/* 继上一个低电平时钟无效数据前一半 */IIC_SCL(0);IIC_SDA((data & 0x80) >> 7);    /* 高位先发送 */iic_delay();IIC_SCL(1);IIC_SDA((data & 0x80) >> 7);    /* 高位先发送 */iic_delay();iic_delay();IIC_SCL(0);IIC_SDA((data & 0x80) >> 7);    /* 高位先发送 */iic_delay();data <<= 1;     /* 左移1位,用于下一次发送 */}
}/* IIC读取一个字节* 返回接收到的数据*/
uint8_t iic_read_byte(void)
{uint8_t i, receive = 0;for (i = 0; i < 8; i++ )    /* 接收1个字节数据 */{receive <<= 1;  /* 高位先输出,所以先收到的数据位要左移 */IIC_SCL(0);IIC_SDA(1);iic_delay();/* 先输出高电平时钟,并且延时1/4时钟,确保读取的是正确的电平信号,如果不延时的话,我们立即读取的电平信号可能是从机还未设置输出对应的数据电平 *//* 而SDA设置高电平,那么SCL和SDA都为高,也就是把主机SDA释放,给从机持有SDA发送数据,那么主机接收从机发送的数据 */IIC_SCL(1);IIC_SDA(1);iic_delay();if (IIC_READ_SDA){receive++;}iic_delay();IIC_SCL(0);iic_delay();}return receive;
}

3、24cxx.h文件

#ifndef __24CXX_H
#define __24CXX_H#include "main.h"#define AT24C01     127
#define AT24C02     255
#define AT24C04     511
#define AT24C08     1023
#define AT24C16     2047
#define AT24C32     4095
#define AT24C64     8191
#define AT24C128    16383
#define AT24C256    32767/* 开发板使用的是24c02,所以定义EE_TYPE为AT24C02 */#define EE_TYPE     AT24C02void at24cxx_init(void);        /* 初始化IIC */
uint8_t at24cxx_check(void);    /* 检查器件 */
uint8_t at24cxx_read_one_byte(uint16_t addr);                       /* 指定地址读取一个字节 */
void at24cxx_write_one_byte(uint16_t addr,uint8_t data);            /* 指定地址写入一个字节 */
void at24cxx_write(uint16_t addr, uint8_t *pbuf, uint16_t datalen); /* 从指定地址开始写入指定长度的数据 */
void at24cxx_read(uint16_t addr, uint8_t *pbuf, uint16_t datalen);  /* 从指定地址开始读出指定长度的数据 */
void at24cxx_write_one_page_byte(uint16_t addr, uint8_t *data, uint16_t datalen);	/* 读取一页,也就是8个字节 */
void at24cxx_read_n_byte(uint16_t addr, uint8_t *data, uint16_t datalen);	/* 从指定地址开始读出指定长度的数据 */#endif

4、24cxx.c文件

#include "myiic.h"
#include "24cxx.h"/*** @brief       初始化IIC接口* @param       无* @retval      无*/
void at24cxx_init(void)
{iic_init();
}/*** @brief       在AT24CXX指定地址读出一个数据* @param       readaddr: 开始读数的地址* @retval      读到的数据*/
uint8_t at24cxx_read_one_byte(uint16_t addr)
{uint8_t temp = 0;iic_start();    /* 发送起始信号 *//* 根据不同的24CXX型号, 发送高位地址* 1, 24C16以上的型号, 分2个字节发送地址* 2, 24C16及以下的型号, 分1个低字节地址 + 占用器件地址的bit1~bit3位 用于表示高位地址, 最多11位地址*    对于24C01/02, 其器件地址格式(8bit)为: 1  0  1  0  A2  A1  A0  R/W*    对于24C04,    其器件地址格式(8bit)为: 1  0  1  0  A2  A1  a8  R/W*    对于24C08,    其器件地址格式(8bit)为: 1  0  1  0  A2  a9  a8  R/W*    对于24C16,    其器件地址格式(8bit)为: 1  0  1  0  a10 a9  a8  R/W*    R/W      : 读/写控制位 0,表示写; 1,表示读;*    A0/A1/A2 : 对应器件的1,2,3引脚(只有24C01/02/04/8有这些脚)*    a8/a9/a10: 对应存储整列的高位地址, 11bit地址最多可以表示2048个位置, 可以寻址24C16及以内的型号*/    if (EE_TYPE > AT24C16)      /* 24C16以上的型号, 分2个字节发送地址 */{iic_send_byte(0xA0);    /* 发送写命令, IIC规定最低位是0, 表示写入 */iic_wait_ack();         /* 每次发送完一个字节,都要等待ACK */iic_send_byte(addr >> 8);   /* 发送高字节地址 */}else {iic_send_byte(0xA0 + ((addr >> 8) << 1));   /* 发送器件 0xA0 + 高位a8/a9/a10地址,写数据 */}iic_wait_ack();             /* 每次发送完一个字节,都要等待ACK */iic_send_byte(addr % 256);  /* 发送低位地址 */iic_wait_ack();             /* 等待ACK, 此时地址发送完成了 */iic_start();                /* 重新发送起始信号 */ iic_send_byte(0xA1);        /* 进入接收模式, IIC规定最低位是1, 表示读取 */iic_wait_ack();             /* 每次发送完一个字节,都要等待ACK */temp = iic_read_byte();    /* 接收一个字节数据后返回非应答信号 */iic_nack();     			/* 发送nACK */    iic_stop();                 /* 产生一个停止条件 */return temp;
}/*** @brief       在AT24CXX指定地址写入一个数据* @param       addr: 写入数据的目的地址* @param       data: 要写入的数据* @retval      无*/
void at24cxx_write_one_byte(uint16_t addr, uint8_t data)
{/* 原理说明见:at24cxx_read_one_byte函数, 本函数完全类似 */iic_start();    /* 发送起始信号 */if (EE_TYPE > AT24C16)      /* 24C16以上的型号, 分2个字节发送地址 */{iic_send_byte(0xA0);    /* 发送写命令, IIC规定最低位是0, 表示写入 */iic_wait_ack();         /* 每次发送完一个字节,都要等待ACK */iic_send_byte(addr >> 8);   /* 发送高字节地址 */}else{iic_send_byte(0xA0 + ((addr >> 8) << 1));   /* 发送器件 0xA0 + 高位a8/a9/a10地址,写数据 */}iic_wait_ack();             /* 每次发送完一个字节,都要等待ACK */iic_send_byte(addr % 256);  /* 发送低位地址 */iic_wait_ack();             /* 等待ACK, 此时地址发送完成了 *//* 因为写数据的时候,不需要进入接收模式了,所以这里不用重新发送起始信号了 */iic_send_byte(data);        /* 发送1字节 */iic_wait_ack();             /* 等待ACK */iic_stop();                 /* 产生一个停止条件 */delay_ms(10);               /* 注意: EEPROM 写入比较慢,必须等到10ms后再写下一个字节 */
}/*** @brief       检查AT24CXX是否正常*   @note      检测原理: 在器件的末地址写如0X55, 然后再读取, 如果读取值为0X55*              则表示检测正常. 否则,则表示检测失败.** @param       无* @retval      检测结果*              0: 检测成功*              1: 检测失败*/
uint8_t at24cxx_check(void)
{uint8_t temp;uint16_t addr = EE_TYPE;temp = at24cxx_read_one_byte(addr);     /* 避免每次开机都写AT24CXX */if (temp == 0x55)   /* 读取数据正常 */{return 0;}else    /* 排除第一次初始化的情况 */{at24cxx_write_one_byte(addr, 0x55); /* 先写入数据 */temp = at24cxx_read_one_byte(255);  /* 再读取数据 */if (temp == 0x55)return 0;}return 1;
}/*** @brief       在AT24CXX里面的指定地址开始读出指定个数的数据* @param       addr    : 开始读出的地址 对24c02为0~255* @param       pbuf    : 数据数组首地址* @param       datalen : 要读出数据的个数* @retval      无*/
void at24cxx_read(uint16_t addr, uint8_t *pbuf, uint16_t datalen)
{while (datalen--){*pbuf++ = at24cxx_read_one_byte(addr++);}
}/*** @brief       在AT24CXX里面的指定地址开始写入指定个数的数据* @param       addr    : 开始写入的地址 对24c02为0~255* @param       pbuf    : 数据数组首地址* @param       datalen : 要写入数据的个数* @retval      无*/
void at24cxx_write(uint16_t addr, uint8_t *pbuf, uint16_t datalen)
{while (datalen--){at24cxx_write_one_byte(addr, *pbuf);addr++;pbuf++;}
}/* 只能连续写入8个字节 */
void at24cxx_write_one_page_byte(uint16_t addr, uint8_t *data, uint16_t datalen)
{/* 原理说明见:at24cxx_read_one_byte函数, 本函数完全类似 */iic_start();    /* 发送起始信号 */if (EE_TYPE > AT24C16)      /* 24C16以上的型号, 分2个字节发送地址 */{iic_send_byte(0xA0);    /* 发送写命令, IIC规定最低位是0, 表示写入 */iic_wait_ack();         /* 每次发送完一个字节,都要等待ACK */iic_send_byte(addr >> 8);   /* 发送高字节地址 */}else{iic_send_byte(0xA0 + ((addr >> 8) << 1));   /* 发送器件 0xA0 + 高位a8/a9/a10地址,写数据 */}iic_wait_ack();             /* 每次发送完一个字节,都要等待ACK */iic_send_byte(addr % 256);  /* 发送低位地址 */iic_wait_ack();             /* 等待ACK, 此时地址发送完成了 *//* 因为写数据的时候,不需要进入接收模式了,所以这里不用重新发送起始信号了 */while (datalen--){iic_send_byte(*data);       	/* 发送1字节 */iic_wait_ack();             	/* 等待ACK */data++;}	iic_stop();                 /* 产生一个停止条件 */
}void at24cxx_read_n_byte(uint16_t addr, uint8_t *data, uint16_t datalen)
{int i;iic_start();    /* 发送起始信号 */if (EE_TYPE > AT24C16)      /* 24C16以上的型号, 分2个字节发送地址 */{iic_send_byte(0xA0);    /* 发送写命令, IIC规定最低位是0, 表示写入 */iic_wait_ack();         /* 每次发送完一个字节,都要等待ACK */iic_send_byte(addr >> 8);   /* 发送高字节地址 */}else {iic_send_byte(0xA0 + ((addr >> 8) << 1));   /* 发送器件 0xA0 + 高位a8/a9/a10地址,写数据 */}iic_wait_ack();             /* 每次发送完一个字节,都要等待ACK */iic_send_byte(addr % 256);  /* 发送低位地址 */iic_wait_ack();             /* 等待ACK, 此时地址发送完成了 */iic_start();                /* 重新发送起始信号 */ iic_send_byte(0xA1);        /* 进入接收模式, IIC规定最低位是1, 表示读取 */iic_wait_ack();             /* 每次发送完一个字节,都要等待ACK */for(i = 0; i < datalen; i++){if(datalen == i+1) {data[i] = iic_read_byte();    /* 接收一个字节数据后返回非应答信号 */iic_nack();     				/* 发送nACK */} else {data[i] = iic_read_byte();    /* 接收一个字节数据后返回应答信号 */iic_ack();      				/* 发送ACK */}}iic_stop();                 /* 产生一个停止条件 */
}

相关文章:

绝对让你明明白白,脚把脚带你盯着 I2C 时序图将 I2C 程序给扣出来(基于STM32的模拟I2C)

目录前言一、关于STM32 I/O端口位的基本结构讲解二、模拟I2C编写前的需知道的知识1、I2C简介2、根据时序编写模拟I2C程序重要的两点Ⅰ、主机发送数据给从机时的时序控制Ⅱ、主机接收来自从机的数据时的时序控制Ⅲ、完整的I2C时序图&#xff08;按写程序的思想分割时序&#xff…...

2023年全国最新工会考试精选真题及答案5

百分百题库提供工会考试试题、工会考试预测题、工会考试真题、工会证考试题库等&#xff0c;提供在线做题刷题&#xff0c;在线模拟考试&#xff0c;助你考试轻松过关。 一、单选题 1.企业工会委员会实行&#xff08;&#xff09;&#xff0c;重要问题须经&#xff08;&#x…...

一文2000字手把手教你自动化测试Selenium+pytest+数据驱动

主流自动化框架 selenium &#xff1a;web端自动化框架 &#xff0c;&#xff08;行业里面最核心的框架&#xff09; appium &#xff1a;手机app端框架 requests &#xff1a;接口测试 selenium 工具类封装 selenium提供了很多方法供我们去完成网页元素的操作&#xff0c; …...

windows安装Ubuntu子系统以及图形化界面记录

文章目录1. windows环境设置2. 开始安装3. ubuntu使用3.1 启动和退出 Linux 子系统3.2 安装位置3.3 更换源4. 安装图形化界面4.1 安装VcXsrv4.2 安装桌面环境&#xff08;1&#xff09;方法1&#xff1a;VcXsrv Gnome&#xff08;2&#xff09;方法2&#xff1a;VcXsrv Xfce4…...

通俗易懂,十分钟读懂DES,详解DES加密算法原理,DES攻击手段以及3DES原理。Python DES实现源码

文章目录1、什么是DES2、DES的基本概念3、DES的加密流程4、DES算法步骤详解4.1 初始置换(Initial Permutation&#xff0c;IP置换)4.2 加密轮次4.3 F轮函数4.3.1 拓展R到48位4.3.2 子密钥K的生成4.3.3 当前轮次的子密钥与拓展的48位R进行异或运算4.3.4 S盒替换&#xff08;Subs…...

为多态基类声明virtual析构函数

我们知道&#xff0c;有时会让一个基类指针指向用 new 运算符动态生成的派生类对象&#xff08;类似接口的作用&#xff09;&#xff1b;同时&#xff0c;用 new 运算符动态生成的对象都是通过 delete 指向它的指针来释放的。如果一个基类指针指向用 new 运算符动态生成的派生类…...

啊哈 算法读书笔记 第 2 章 栈、队列、链表

第 2 章 栈、队列、链表 目录 第 2 章 栈、队列、链表 队列&#xff1a; 解密回文——栈 纸牌游戏&#xff1a; 链表 模拟链表 队列&#xff1a; 首先将第 1 个数删除&#xff0c;紧接着将第 2 个数放到这串数的末尾&#xff0c;再将第 3 个数删除并将第 4 个数放到这串…...

Git ---- IDEA 集成 Git

Git ---- IDEA 集成 Git1. 配置 Git 忽略文件2. 定位 Git 程序3. 初始化本地库4. 添加到暂存区5. 提交到本地库6. 切换版本7. 创建分支8. 切换分支9. 合并分支10. 解决冲突1. 配置 Git 忽略文件 1. Eclipse 特定文件 2. IDEA 特定文件 3. Maven 工程的 target 目录 问题1…...

【LeetCode 704】【Go】二分查找

二分查找题解 一、碎碎念 从本周开始&#xff0c;重新更新刷题记录了哈。 基于费曼学习法的原理&#xff0c;最好的输入是输出&#xff0c;所以与大家分享。 鉴于目前这个糟糕的市场环境&#xff0c;还是要练好自己的基本技术&#xff0c;万一那天就被迫 N 1了&#xff0c;你…...

【代码随想录训练营】【Day23】第六章|二叉树|669. 修剪二叉搜索树 |108.将有序数组转换为二叉搜索树|538.把二叉搜索树转换为累加树

修剪二叉搜索树 题目详细&#xff1a;LeetCode.669 做这道题之前建议先看视频讲解&#xff0c;没有想象中那么复杂&#xff1a;代码随想录—修剪二叉搜索树 由题可知&#xff0c;需要删除节点值不在区间内的节点&#xff0c;所以可以得到三种情况&#xff1a; 情况一&#…...

CV——day78 读论文:通过静态背景构建扩展低通道路边雷达的探测距离(目标是规避风险)

Extending the Detection Range for Low-Channel Roadside LiDAR by Static Background Construction 通过静态背景构建扩展低通道路边雷达的探测距离I. INTRODUCTIONII. RELATED WORKA. LiDAR-Based 3-D Vehicle and Road User DetectionB. LiDAR Data Background FilteringC.…...

【编程入门】应用市场(go语言版)

背景 前面已输出多个系列&#xff1a; 《十余种编程语言做个计算器》 《十余种编程语言写2048小游戏》 《17种编程语言10种排序算法》 《十余种编程语言写博客系统》 《十余种编程语言写云笔记》 《N种编程语言做个记事本》 目标 为编程初学者打造入门学习项目&#xff0c;使…...

Linux(openEuler)没有界面连接互联网方法

前言: 系统版本openEuleropenEuler-22.03-LTS-x86_64-dvd 我们在安装linux之后&#xff0c;一般都是无界面的情况。大部分情况都是需要自己安装界面的&#xff0c;如果路由器的情况下直接插上网络就好了。下面就开始介绍两种方法进行linxu网络的连接。 注意: 小编是使用的第一…...

第一天 软考中级--嵌入式系统设计师考试复习教程开始了

第一天 嵌入式系统设计师考试复习教程 第二天 软考中级--嵌入式系统设计师考试考试大纲解析 目录...

分享 10 个高频 Python 面试题

Python 很容易学会&#xff0c;但很难掌握。你可以在几天内了解它的基本语法&#xff0c;但是要能够用 Python 开发出足够好的商业软件&#xff0c;多年的实践是必须的。因为&#xff0c;无论你使用哪种编程语言&#xff0c;你都必须对其复杂的内部机制有足够的了解&#xff0c…...

ThreadLocal原理、结构、源码解析

文章目录一、Thread简介1.什么是ThreadLocal2.为什么要是用ThreadLocal2.1Synchronized、Lock保证线程安全2.2ThreadLocal保证线程安全3.ThreadLocal和Synchronized的区别二、ThreadLocal原理1.Thread抽象内部结构2.ThreadLocal源码2.1Thread、ThreadLocal、ThreadLocalMap、En…...

分布式之PBFT算法

写在前面 在分布式之拜占庭问题 一文中我们分析了拜占庭问题&#xff0c;并一起看了支持拜占庭容错的口信消息性和签名消息性算法&#xff0c;但是这两种算法都有一个非常严重的问题&#xff0c;就是消息数量太多&#xff0c;通信的成本太大&#xff0c;消息数量复杂度为O(n ^…...

Linux 操作系统——查看/修改系统时区、时间、本地时间修改为UTC

文章目录1.背景描述2.知识储备3.解决步骤1. 查看当前时区2.修改设置Linux服务器时区3.复制相应的时区文件&#xff0c;替换系统时区文件&#xff1b;或者创建链接文件4. 查看和修改Linux的时间5. 硬件时间和系统时间的 相互同步1.背景描述 最近一个项目日期采用java8的LocalDa…...

CSS数据类型以及符号

css数据类型定义的是css属性中具有代表性的值&#xff0c;在规范的语法格式中&#xff0c;使用关键字外加一对 <和>表示&#xff0c;例如数值类型<number>、色值类型<color>等。 举个例子&#xff1a;background-image这个css属性语法结构如下&#xff1a; …...

LeetCode-54. 螺旋矩阵

题目来源 54. 螺旋矩阵 题目思路 while循环只遍历"环"&#xff0c;不成环就不遍历了 四个边界 上边界 top : 0下边界 bottom : matrix.length - 1左边界 left : 0右边界 right : matrix[0].length - 1 矩阵不一定是方阵 top < bottom && left < r…...

【Python入门第十八天】Python For 循环

Python For 循环 for 循环用于迭代序列&#xff08;即列表&#xff0c;元组&#xff0c;字典&#xff0c;集合或字符串&#xff09;。 这与其他编程语言中的 for 关键字不太相似&#xff0c;而是更像其他面向对象编程语言中的迭代器方法。 通过使用 for 循环&#xff0c;我们…...

Qt图片定时滚动播放器

目录参考结构PicturePlay.promain.cpppictureplay.hpictureplay.cpppictureplay.ui效果源码参考 Qt图片浏览器 QT制作一个图片播放器 Qt中自适应的labelpixmap充满窗口后&#xff0c;无法缩小只能放大 可以显示jpg、jpeg、png、bmp。可以从电脑上拖动图到窗口并显示出来或者打开…...

李宏毅2023春季机器学习课程

目录2021&2022课程重磅须知我维护的其他项目更新日志课程地址课程资料直链课程作业直链其他优质课程2021&2022课程 CSDN Github 重磅须知 为方便所有网课资料与优质电子书籍的实时更新维护&#xff0c;创建一个在线实时网盘文件夹&#xff1b;   网盘获取方式&#…...

计算机操作系统知识点汇总

计算机操作系统选择填空题&#xff0c;300知识点&#xff0c;包含操作系统概论、处理机管理、内存管理、设备管理、文件管理等&#xff0c;为大学生期末创造奇迹提供无限可能 1、填空题 1、操作系统是对计算机资源进行管理的软件 2、操作系统是提供了处理机管理、 存储器管理…...

【离线数仓-8-数据仓库开发DWD层设计要点-交易域相关事实表】

离线数仓-8-数据仓库开发DWD层设计要点-交易域相关事实表离线数仓-8-数据仓库开发DWD层设计要点-交易域相关事实表一、DWD层设计要点二、交易域相关事实表1.交易域加购事务事实表1.加购事务事实表 前期梳理2.加购事务事实表 DDL表设计分析3.加购事务事实表 加载数据分析1.首日全…...

计算机网络(七):DNS协议和原理,DNS为什么用UDP,网页解析的全过程

文章目录一、什么是DNS二、DNS的作用三、DNS作用四、DNS为什么用UDP五、如果打开一个网站很慢&#xff0c;要如何排查六、网页解析的全过程一、什么是DNS DNS是域名系统的英文缩写&#xff0c;是一种组织成域层次结构的计算机和网络服务命名系统&#xff0c;用于TCP/IP网络。 …...

算法23:多叉树_派对的最大快乐值

公司的每个员工都符合 Employee 类的描述。整个公司的人员结构可以看作是一棵标准的、 没有环的多叉树。树的头节点是公司唯一的老板。除老板之外的每个员工都有唯一的直接上级。 叶节点是没有任何下属的基层员工(subordinates列表为空)&#xff0c;除基层员工外&#xff0c;每…...

中国ETC行业市场规模及未来发展趋势

中国ETC行业市场规模及未来发展趋势编辑根据市场调研在线网发布的2023-2029年中国ETC行业发展策略分析及战略咨询研究报告分析&#xff1a;随着政府坚持实施绿色出行政策&#xff0c;ETC行业也受到了极大的支持。根据中国智能交通协会统计&#xff0c;2017年中国ETC行业市场规模…...

每日刷题(一)——只出现一次的数字

前言 今天遇到一个位运算的题目&#xff0c;感觉很有意思&#xff0c;记录一下。 Question1 136. 只出现一次的数字 给你一个 非空 整数数组 nums &#xff0c;除了某个元素只出现一次以外&#xff0c;其余每个元素均出现两次。找出那个只出现了一次的元素。 你必须设计并实…...

洛谷P5737 【深基7.例3】闰年展示 C语言/C++

【深基7.例3】闰年展示 题目描述 输入 x,yx,yx,y&#xff0c;输出 [x,y][x,y][x,y] 区间中闰年个数&#xff0c;并在下一行输出所有闰年年份数字&#xff0c;使用空格隔开。 输入格式 输入两个正整数 x,yx,yx,y&#xff0c;以空格隔开。 输出格式 第一行输出一个正整数&a…...