【STM32】IIC的初步使用
IIC简介
物理层

连接多个devices
它是一个支持设备的总线。“总线”指多个设备共用的信号线。在一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。
两根线
一个 I2C 总线只使用两条总线线路,一条双向串行数据线(SDA) ,一条串行时钟线(SCL)。数据线即用来表示数据,时钟线用于数据收发同步。
设备地址
每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问。
上拉电阻和高阻态
总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。
仲裁
多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。
三种速度模式
具有三种传输模式:标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式下可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式。
最大devices限制
连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制 。
协议层
I2C 的协议定义了通讯的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。
I2C 基本读写过程

主机写数据到从机

主机由从机中读数据

I2C 通讯复合格式
数据由主机传输至从机
数据由从机传输至主机
: 传输开始信号
: 传输方向选择位,1 为读,0 为写
: 应答(ACK)或非应答(NACK)信号
停止传输信号
1.S是起始信号,由主机产生,挂载在总线上的设备都会收到,准备接收主机下一个数据
2.设备聆听地址信息,选择从机: 起始信号之后,主机会广播从机地址,在 I2C 总线上,每个设备的地址都是唯一的,当主机广播的地址与某个设备地址相同时,这个设备就被选中了,没被选中的设备将会忽略之后的数据信号。根据 I2C 协议,这个从机地址可以是 7 位或 10 位。
3.传输方向:主机广播传输方向,0是主机到从机(主机往从机写),1为从机到主机(从机往主机写)
4.从机应答:回复ack与nack,只有收到ack之后,主机继续发送或者接收数据
写数据

主机首先在 IIC 总线上发送起始信号,那么这时总线上的从机都会等待接收由主机发出的数据。主机接着发送从机地+0(写操作)组成的 8bit 数据,所有从机接收到该 8bit 数据后,自行检验是否是自己的设备的地址,假如是自己的设备地址,那么从机就会发出应答信号。主机在总线上接收到有应答信号后,才能继续向从机发送数据。注意:IIC 总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。
读数据

若配置的方向传输位为“读数据”方向,即第二幅图的情况,广播完地址,接收到应答信号后,从机开始向主机返回数据(DATA),数据包大小也为 8 位,从机每发送完一个数据,都会等待主机的应答信号(ACK),重复这个过程,可以返回 N 个数据,这个 N 也没有大小限制。当主机希望停止接收数据时,就向从机返回一个非应答信号(NACK),则从机自动停止数据传输。
信号电平
起始 停止信号

当 SCL 线是高电平时 SDA 线从高电平向低电平切换,这个情况表示通讯的起始。当 SCL 是高电平时 SDA 线由低电平向高电平切换,表示通讯的停止。起始和停止信号一般由主机产生。
数据有效性 SCL的高电平

SCL为高电平的时候 SDA表示的数据有效,SCL为低电平时数据变换
地址及数据方向
I2C 总线上的每个设备都有自己的独立地址,主机发起通讯时,通过 SDA 信号线发送设备地址(SLAVE_ADDRESS)来查找从机。I2C 协议规定设备地址可以是 7 位或 10 位,实际中 7 位的地址应用比较广泛。紧跟设备地址的一个数据位用来表示数据传输方向。
读数据方向时,主机会释放对 SDA 信号线的控制,由从机控制 SDA 信号线,主机接收信号,写数据方向时,SDA 由主机控制,从机接收信号。

大端(高有效位存在低地址),波形高位先行
ACK NACK
主站发送起始信号,地址,读写信号之后释放SDA控制权,从站开始自动控制SDA信号发送ACK NACK
整体控制逻辑
整体控制逻辑负责协调整个 I2C 外设,控制逻辑的工作模式根据我们配置的“控制寄存器(CR1/CR2)”的参数而改变。在外设工作时,控制逻辑会根据外设的工作状态修改“状态寄存器(SR1 和 SR2)”,我们只要读取这些寄存器相关的寄存器位,就可以了解 I2C的工作状态。除此之外,控制逻辑还根据要求,负责控制产生 I2C 中断信号、DMA 请求及各种 I2C 的通讯信号(起始、停止、响应信号等)。
IIC使用 读写EEPROM为例
要点
(1) 配置通讯使用的目标引脚为开漏模式;
(2) 使能 I2C 外设的时钟;
(3) 配置 I2C 外设的模式、地址、速率等参数并使能 I2C 外设;
(4) 编写基本 I2C 按字节收发的函数;
(5) 编写读写 EEPROM 存储内容的函数;
硬件IIC
配置IIC复用的GPIO
#配置 GPIO 复用
GPIO_InitTypeDef GPIO_InitStructure;/* 使能与 I2C 有关的时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB)/* 配置GPIO为开漏输出模式 */
//配置Gpio初始化结构体略
GPIO_Init(GPIOB, &GPIO_InitStructure);
1.配置IIC模式
I2C_InitTypeDef I2C_InitStructure;/* I2C 配置 */
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;/*!< 指定工作模式,可选 I2C 模式及 SMBUS 模式 *//* 高电平数据稳定,低电平数据变化 SCL 时钟线的占空比 */
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; /*指定时钟占空比,可选 low/high = 2:1 及 16:9 模式*//*!< 指定自身的 I2C 设备地址 */
I2C_InitStructure.I2C_OwnAddress1 =I2Cx_OWN_ADDRESS7;/*!< 指定自身的 I2C 设备地址 */I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ; /*!< 使能或关闭响应(一般都要使能) *//* I2C 的寻址模式 */
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; /*!< 指定地址的长度,可为 7 位及 10 位 */I2C_InitStructure.I2C_ClockSpeed = I2C_Speed; /*!< 设置 SCL 时钟频率,此值要低于 400000*/I2C_Init(I2C1, &I2C_InitStructure);
I2C_Cmd(I2C1, ENABLE);
1.1作为主机端,进行数据发送
即作为 I2C 通讯的主机端时,对外部发送数据的过程
以写一个E2ROM为例

(1) 控制产生起始信号(S),当发生起始信号后,它产生事件“EV5”,并会对 SR1 寄存器的“SB”位置 1,表示起始信号已经发送;
(2) 紧接着发送设备地址并等待应答信号,若有从机应答,则产生事件“EV6”及“EV8”,这时 SR1 寄存器的“ADDR”位及“TXE”位被置 1,ADDR 为 1 表示地址已经发送,TXE 为 1 表示数据寄存器为空;
(3) 以上步骤正常执行并对 ADDR 位清零后,我们往 I2C 的“数据寄存器 DR”写入要发送的数据,这时TXE位会被重置0,表示数据寄存器非空,I2C外设通过SDA信号线一位位把数据发送出去后,又会产生“EV8”事件,即 TXE 位被置 1,重复这个过程,就可以发送多个字节数据了;
(4) 当我们发送数据完成后,控制 I2C 设备产生一个停止信号§,这个时候会产生EV8_2 事件,SR1 的 TXE 位及 BTF 位都被置 1,表示通讯结束。
假如我们使能了 I2C 中断,以上所有事件产生时,都会产生 I2C 中断信号,进入同一个中断服务函数,到 I2C 中断服务程序后,再通过检查寄存器位来判断是哪一个事件。
IIC STM32固件库函数链接: IIC STM32固件库函数链接
1.1.1发送单字节数据

AT24C02的单字节时序规定,向它写入数据的时候,第一个字节为内存地址,第二个字节是要写入的数据内容。在发送device地址之后,需要发送eeprom的内部存储地址和需要存的数据
#define EEPROM_I2Cx I2C1/* 具体修改的寄存器位置,查看固件库函数*/uint32_t I2C_EE_ByteWrite(u8* pBuffer, u8 WriteAddr){/* 产生 I2C 起始信号 *///I2Cx->CR1 |= I2C_CR1_START; 操作I2C中的CR1寄存器I2C_GenerateSTART(I2C1, ENABLE);/*设置超时等待时间*/I2CTimeout = I2CT_FLAG_TIMEOUT;//I2CTimeout 常数/* 检测 EV5 事件并清除标志*/while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT)){/* 超时,报错*/if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(0);}/* 发送 EEPROM 设备地址 */I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS,I2C_Direction_Transmitter);/* 检测 EV6 事件并清除标志*/while (!I2C_CheckEvent(EEPROM_I2Cx,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(1);}/* 发送要写入的 EEPROM 内部地址(即 EEPROM 内部存储器的地址) *//* EEPROM 内部存储器读写的规则是第一个数据为要读写的地址,第二个数据是具体读写的数据 */I2C_SendData(EEPROM_I2Cx, WriteAddr);I2CTimeout = I2CT_FLAG_TIMEOUT;/* 检测 EV8 事件并清除标志*/while (!I2C_CheckEvent(EEPROM_I2Cx,I2C_EVENT_MASTER_BYTE_TRANSMITTED)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(2);}/* 发送一字节要写入的数据 */I2C_SendData(EEPROM_I2Cx, *pBuffer);I2CTimeout = I2CT_FLAG_TIMEOUT;I2CTimeout = I2CT_FLAG_TIMEOUT;/* 检测 EV8 事件并清除标志*/while (!I2C_CheckEvent(EEPROM_I2Cx,I2C_EVENT_MASTER_BYTE_TRANSMITTED)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(2);}/* 发送停止信号 */I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);}
1.1.2发送多字节数据
等待存储设备准备好
这个函数主要实现是向 EEPROM 发送它设备地址,检测 EEPROM 的响应,若EEPROM 接收到地址后返回应答信号,则表示 EEPROM 已经准备好,可以开始下一次通讯。函数中检测响应是通过读取 STM32 的 SR1 寄存器的 ADDR 位及 AF 位来实现的,当I2C 设备响应了地址的时候,ADDR 会置 1,若应答失败,AF 位会置 1。
//等待 EEPROM 到准备状态void I2C_EE_WaitEepromStandbyState(void){vu16 SR1_Tmp = 0;do {/* 发送起始信号 */I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);/* 读 I2C1 SR1 寄存器 */SR1_Tmp = I2C_ReadRegister(EEPROM_I2Cx, I2C_Register_SR1);/* 发送 EEPROM 地址 + 写方向 */I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS,I2C_Direction_Transmitter);}// SR1 位 1 ADDR:1 表示地址发送成功,0 表示地址发送没有结束// 等待地址发送成功while (!(I2C_ReadRegister(EEPROM_I2Cx, I2C_Register_SR1) & 0x0002));/* 清除 AF 位 */I2C_ClearFlag(EEPROM_I2Cx, I2C_FLAG_AF);/* 发送停止信号 */I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
}
循环单字节发送
uint8_t I2C_EE_ByetsWrite(uint8_t* pBuffer,uint8_t WriteAddr,uint16_t NumByteToWrite)
{uint16_t i;uint8_t res;/*每写一个字节调用一次 I2C_EE_ByteWrite 函数,前文的单字节写入*/for (i=0; i<NumByteToWrite; i++){/*等待 EEPROM 准备完毕*/I2C_EE_WaitEepromStandbyState();/*按字节写入数据*/res = I2C_EE_ByteWrite(pBuffer++,WriteAddr++);}return res;
}
1.1.3 EEPROM 的页写入
EEPROM 定义了一种页写入时序,只要告诉 EEPROM 第一个内存地址 address1,后面的数
据按次序写入到 address2、address3… 这样可以节省通讯的时间,加快速度。

根据页写入时序,第一个数据被解释为要写入的内存地址 address1,后续可连续发送 n个数据,这些数据会依次写入到内存中。其中 AT24C02 型号的芯片页写入时序最多可以一次发送 8 个数据(即 n = 8 ),该值也称为页大小,某些型号的芯片每个页写入时序最多可传输 16 个数据。
但是这种方式还是比较慢,拘泥于页的大小。
//在 EEPROM 的一个写循环中可以写多个字节,但一次写入的字节数不能超过 EEPROM 页的大小,AT24C02 //每页有 8 个字节
//NumByteToWrite:要写的字节数要求 NumByToWrite 小于页大小uint8_t I2C_EE_PageWrite(uint8_t* pBuffer, uint8_t WriteAddr,uint8_t NumByteToWrite)
{ I2CTimeout = I2CT_LONG_TIMEOUT;while (I2C_GetFlagStatus(EEPROM_I2Cx, I2C_FLAG_BUSY)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(4);}/* 产生 I2C 起始信号 */I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);I2CTimeout = I2CT_FLAG_TIMEOUT;/* 检测 EV5 事件并清除标志 */while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(5);}/* 发送 EEPROM 设备地址 */I2C_Send7bitAddress(EEPROM_I2Cx,EEPROM_ADDRESS,I2C_Direction_Transmitter);I2CTimeout = I2CT_FLAG_TIMEOUT;/* 检测 EV6 事件并清除标志*/while (!I2C_CheckEvent(EEPROM_I2Cx,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(6);}/* 发送要写入的 EEPROM 内部地址(即 EEPROM 内部存储器的地址) */I2C_SendData(EEPROM_I2Cx, WriteAddr);I2CTimeout = I2CT_FLAG_TIMEOUT;/* 检测 EV8 事件并清除标志*/while (! I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(7);}/* 循环发送 NumByteToWrite 个数据 */while (NumByteToWrite--){/* 发送缓冲区中的数据 */I2C_SendData(EEPROM_I2Cx, *pBuffer);/* 指向缓冲区中的下一个数据 */pBuffer++;I2CTimeout = I2CT_FLAG_TIMEOUT;/* 检测 EV8 事件并清除标志*/while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(8);}}/* 发送停止信号 */I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
}
1.1.4 EEPROM 的页写入plus
利用 EEPROM 的页写入方式,可以改进前面的“多字节写入”函数,加快传输速度
// AT24C01/02 每页有 8 个字节#define I2C_PageSize 8void I2C_EE_BufferWrite(u8* pBuffer, u8 WriteAddr,u16 NumByteToWrite){u8 NumOfPage=0,NumOfSingle=0,Addr =0,count=0,temp =0;/*mod 运算求余,若 writeAddr 是 I2C_PageSize 整数倍,运算结果 Addr 值为 0*//*判断目标地址(起始地址)是否为一页的开始*/Addr = WriteAddr % I2C_PageSize;/*差 count 个数据值,刚好可以对齐到页地址*/count = I2C_PageSize - Addr;/*计算出要写多少整数页*/NumOfPage = NumByteToWrite / I2C_PageSize;/*mod 运算求余,计算出剩余不满一页的字节数*/NumOfSingle = NumByteToWrite % I2C_PageSize;// Addr=0,则 WriteAddr 刚好按页对齐 aligned,也就是从这页的第一个byte开始// 这样就很简单了,直接写就可以,写完整页后, 把剩下的不满一页的写完即可if (Addr == 0) {//从某页的第一位写起/* 如果 NumByteToWrite < I2C_PageSize *//* 总字数还不满一页 */if (NumOfPage == 0) {//总页数不满一页I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);I2C_EE_WaitEepromStandbyState();}/* 如果 NumByteToWrite > I2C_PageSize */else {/*先把整数页都写了*/while (NumOfPage--) {I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);//写一页I2C_EE_WaitEepromStandbyState();//等待rom准备好WriteAddr += I2C_PageSize; //写完一页之后,下一页的地址pBuffer += I2C_PageSize; //写完一页之后,要传输的源地址也要加}/*若有多余的不满一页的数据,把它写完*/if (NumOfSingle!=0) {I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);I2C_EE_WaitEepromStandbyState();}}}从某页的第一位写起// 如果 WriteAddr 不是按 I2C_PageSize 对齐// 那就算出对齐到页地址还需要多少个数据,先把这几个数据写完,剩下开始的地址就已经对齐else{//不是从某页的第一位写起/* 如果 NumByteToWrite < I2C_PageSize */if (NumOfPage== 0){//总数不满一页if (NumOfSingle > count) {//count:还需要写多少个字节才能页面对齐//NumOfSingle:当前页面空闲的字节数// temp 的数据要写到写一页temp = NumOfSingle - count;I2C_EE_PageWrite(pBuffer, WriteAddr, count);//写count数目的到当前页I2C_EE_WaitEepromStandbyState();//等待准备完毕WriteAddr += count;//目标地址移动pBuffer += count;I2C_EE_PageWrite(pBuffer, WriteAddr, temp);//在下一页写完剩下的I2C_EE_WaitEepromStandbyState();}else{ /*若 count 比 NumOfSingle 大*/I2C_EE_PageWrite(pBuffer, WriteAddr, NumByteToWrite);I2C_EE_WaitEepromStandbyState();}}/* 如果 NumByteToWrite > I2C_PageSize *//* 如果 不止写一页 */else { /* 如果 不止写一页 *//*地址不对齐多出的 count 分开处理,不加入这个运算*/NumByteToWrite -= count;NumOfPage = NumByteToWrite / I2C_PageSize;NumOfSingle = NumByteToWrite % I2C_PageSize;/*先把 WriteAddr 所在页的剩余字节写了*/if (count != 0) {I2C_EE_PageWrite(pBuffer, WriteAddr, count);I2C_EE_WaitEepromStandbyState();/*WriteAddr 加上 count 后,地址就对齐到页了*/WriteAddr += count;pBuffer += count;}/*把整数页都写了*/while (NumOfPage--) {I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);I2C_EE_WaitEepromStandbyState();WriteAddr += I2C_PageSize;pBuffer += I2C_PageSize;}/*若有多余的不满一页的数据,把它写完*/if (NumOfSingle != 0) {I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);I2C_EE_WaitEepromStandbyState();}}/*把整数页都写了*/}/* 如果 不止写一页 */ }
1.2作为主机端,进行数据接收
即作为 I2C 通讯的主机端时,从外部接收数据的过程

(1) 同主发送流程,起始信号(S)是由主机端产生的,控制发生起始信号后,它产生事件“EV5”,并会对 SR1 寄存器的“SB”位置 1,表示起始信号已经发送;
(2) 紧接着发送设备地址并等待应答信号,若有从机应答,则产生事件“EV6”这时SR1 寄存器的“ADDR”位被置 1,表示地址已经发送。
(3) 从机端接收到地址后,开始向主机端发送数据。当主机接收到这些数据后,会产生“EV7”事件,SR1 寄存器的 RXNE被置 1,表示接收数据寄存器非空,我们读取该寄存器后,可对数据寄存器清空,以便接收下一次数据。此时我们可以控制I2C 发送应答信号(ACK)或非应答信号(NACK),若应答,则重复以上步骤接收数据,若非应答,则停止传输;
(4) 发送非应答信号后,产生停止信号P,结束传输。
uint8_t I2C_EE_BufferRead(uint8_t* pBuffer, uint8_t ReadAddr,u16 NumByteToRead){I2CTimeout = I2CT_LONG_TIMEOUT;while (I2C_GetFlagStatus(EEPROM_I2Cx, I2C_FLAG_BUSY)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(9);}/* 产生 I2C 起始信号 */I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);I2CTimeout = I2CT_FLAG_TIMEOUT;/* 检测 EV5 事件并清除标志*/while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(10);}/* 发送 EEPROM 设备地址 */I2C_Send7bitAddress(EEPROM_I2Cx,EEPROM_ADDRESS,I2C_Direction_Transmitter);I2CTimeout = I2CT_FLAG_TIMEOUT;/* 检测 EV6 事件并清除标志*/while (!I2C_CheckEvent(EEPROM_I2Cx,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(11);} /*通过重新设置 PE 位清除 EV6 事件 */I2C_Cmd(EEPROM_I2Cx, ENABLE);/* 发送要读取的 EEPROM 内部地址(即 EEPROM 内部存储器的地址) *//* EEPROM 内部存储器读写的规则是第一个数据为要读写的地址,第二个数据是具体读写的数据 */I2C_SendData(EEPROM_I2Cx, ReadAddr);I2CTimeout = I2CT_FLAG_TIMEOUT;/* 检测 EV8 事件并清除标志*/while (!I2C_CheckEvent(EEPROM_I2Cx,I2C_EVENT_MASTER_BYTE_TRANSMITTED)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(12);}/* 产生第二次 I2C 起始信号 */I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);I2CTimeout = I2CT_FLAG_TIMEOUT; /* 检测 EV5 事件并清除标志*/while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(13);}/* 发送 EEPROM 设备地址 */I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS, I2C_Direction_Receiver);I2CTimeout = I2CT_FLAG_TIMEOUT;/* 检测 EV6 事件并清除标志*/while (!I2C_CheckEvent(EEPROM_I2Cx,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(14);}/* 读取 NumByteToRead 个数据*/while (NumByteToRead){/*若 NumByteToRead=1,表示已经接收到最后一个数据了,发送非应答信号,结束传输*/if (NumByteToRead == 1){/* 发送非应答信号 */I2C_AcknowledgeConfig(EEPROM_I2Cx, DISABLE);/* 发送停止信号 */I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);}I2CTimeout = I2CT_LONG_TIMEOUT;while (I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_RECEIVED)==0){if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);}{/*通过 I2C,从设备中读取一个字节的数据 */*pBuffer = I2C_ReceiveData(EEPROM_I2Cx);/* 存储数据的指针指向下一个地址 */pBuffer++;/* 接收数据自减 */NumByteToRead--;} } /* 读取 NumByteToRead 个数据*//* 使能应答,方便下一次 I2C 传输 */I2C_AcknowledgeConfig(EEPROM_I2Cx, ENABLE);}
1.3作为从机端,进行数据接收

1.4作为从机端,进行数据接收

GPIO模拟IIC
我之前使用这套方法读取一个外置的RTC模块,参考正点原子的例程,这边就不赘述了。或者之后有机会把例程贴出来哈
1.配置GPIO
2.编写微秒级软延时
3.根据时序编写各个函数
DMA 与IIC
可以使用DMA,对IIC的DR中要传输或者接收的数据进行搬运,DMA请求(当被使能时)仅用于数据传输。发送时数据寄存器变空或接收时数据寄存器变满,则产生DMA请求。DMA请求必须在当前字节传输结束之前被响应。当为相应DMA通道设置的数据传输量已经完成时,DMA控制器发送传输结束信号ETO到I2C接口,并且在中断允许时产生一个传输完成中断。
主发送器:在EOT中断服务程序中,需禁止DMA请求,然后在等到BTF事件后设置停止条件。
主接收器:当要接收的数据数目大于或等于2时,DMA控制器发送一个硬件信号EOT_1,它对应DMA传输(字节数-1)。如果在I2C_CR2寄存器中设置了LAST位,硬件在发送完EOT_1后的下一个字节,将自动发送NACK。在中断允许的情况下,用户可以在DMA传输完成的中断服务程序中产生一个停止条件。
意思是说当DMA产生EOT标志后,(如果开启了EOT相关中断就进中断程序,没有开启就进行软件查询做后续处理)关闭DMA请求,然后等待BTF事件,之后执行STOP操作。 这里的BTF事件就是I2C数据收发过程中的数据字节是否传输完成的的事件。
IIC通讯过程 标志位
配置DMA
对DMA进行配置
在IIC的配置中开启DMA
通过设置I2C_CR2寄存器中的DMAEN位可以激活DMA模式。只要TxE位被置位,数据将由DMA从预置的存储区装载进I2C_DR寄存器。为I2C分配一个DMA通道,须执行以下步骤(x是通道号)。
利用DMA发送
-
在DMA_CPARx寄存器中设置I2C_DR寄存器地址。数据将在每个TxE事件后从存储器传
送至这个地址。 -
在DMA_CMARx寄存器中设置存储器地址。数据在每个TxE事件后从这个存储区传送至
I2C_DR。 -
在DMA_CNDTRx寄存器中设置所需的传输字节数。在每个TxE事件后,此值将被递减。
-
利用DMA_CCRx寄存器中的PL[0:1]位配置通道优先级。
-
设置DMA_CCRx寄存器中的DIR位。
-
根据应用要求可以配置在整个传输完成一半或全部完成时发出中断请求。
-
通过设置DMA_CCTx寄存器上的EN位激活通道。
当DMA控制器中设置的数据传输数目已经完成时,DMA控制器给I2C接口发送一个传输结束的EOT/ EOT_1信号。在中断允许的情况下,将产生一个DMA中断。
如果使用DMA进行发送时,不要设置I2C_CR2寄存器的ITBUFEN位。
利用DMA接收
通过设置I2C_CR2寄存器中的DMAEN位可以激活DMA接收模式。每次接收到数据字节时,将由DMA把I2C_DR寄存器的数据传送到设置的存储区(参考DMA说明)。设置DMA通道进行I2C接收,须执行以下步骤(x是通道号):
- 在DMA_CPARx寄存器中设置I2C_DR寄存器的地址。数据将在每次RxNE事件后从此地
址传送到存储区。 - 在DMA_CMARx寄存器中设置存储区地址。数据将在每次RxNE事件后从I2C_DR寄存器
传送到此存储区。 - 在DMA_CNDTRx寄存器中设置所需的传输字节数。在每个RxNE事件后,此值将被递
减。 - 用DMA_CCRx寄存器中的PL[0:1]配置通道优先级。
- 清除DMA_CCRx寄存器中的DIR位,根据应用要求可以设置在数据传输完成一半或全部
完成时发出中断请求。 - 设置DMA_CCRx寄存器中的EN位激活该通道。
当DMA控制器中设置的数据传输数目已经完成时,DMA控制器给I2C接口发送一个传输结束的
EOT/ EOT_1信号。在中断允许的情况下,将产生一个DMA中断。
注: 如果使用DMA进行接收时,不要设置I2C_CR2寄存器的ITBUFEN位
相关文章:
【STM32】IIC的初步使用
IIC简介 物理层 连接多个devices 它是一个支持设备的总线。“总线”指多个设备共用的信号线。在一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。 两根线 一个 I2C 总线只使用两条总线线路,一条双向串行数…...
音视频 ffmpeg命令参数说明
主要参数: -i 设定输入流 -f 设定输出格式(format) -ss 开始时间 -t 时间长度 音频参数: -aframes 设置要输出的音频帧数 -b:a 音频码率 -ar 设定采样率 -ac 设定声音的Channel数 -acodec 设定声音编解码器,如果用copy表示原始编解码数据必须…...
Go学习第十天
打印报错堆栈信息 安装errors包 go get github.com/pkg/errors 具体使用 // 新生成一个错误, 带堆栈信息 func New(message string) error//只附加新的信息 func WithMessage(err error, message string) error//只附加调用堆栈信息 func WithStack(err error) error//同时附…...
pytorch中 nn.Conv2d的简单用法
torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride1, padding0, dilation1, groups1, biasTrue,padding_modezeros)参数介绍: in_channels:卷积层输入通道数 out_channels:卷积层输出通道数 kernel_size:卷积层的…...
前端项目工程化之代码规范
目录 一、前言二、ESLint三、Prettier四、项目实战4.1 环境依赖版本4.2 使用pnpm4.3 git提交规范 五、资源 收集六、源码地址 一、前言 前端项目工程化之代码规范是指在前端项目中定义一套代码规范,以确保项目中的代码风格和格式一致,提高代码的可读性和…...
MyBaits Generator
参考文档 MyBatis Generator Core – Introduction to MyBatis Generator MyBatis Generator 详解_enablesubpackages_isea533的博客-CSDN博客 一文解析 MyBatis Generator 的使用及配置 - 掘金 1. Introduction MyBatis Generator (MBG) 是 MyBatis MyBatis的代码生成器。…...
JavaWeb 速通Ajax
目录 一、Ajax快速入门 1.基本介绍 : 2.使用原理 : 二、Ajax经典入门案例 1.需求 : 2.前端页面实现 : 3. 处理HTTP请求的servlet实现 4.引入jar包及druid配置文件、工具类 : 5.Domain层实现 : 6.DAO层实现 : 7.Service层实现 : 8.运行测试 : 三、JQuery操作Ajax 1 …...
vscode c++编译时报错
文章目录 1. 报错内容:GDB Failed with message;2. 报错内容:Unable to start debugging. 1. 报错内容:GDB Failed with message; 例如上图报错,一般就是编译器选择错误,有两种方法解决: 打开 tasks.json …...
基于体系结构架构设计-架构真题(十五)
基于体系结构开发设计(Architecture-Base Software Design)ABSD,是指构成体系结构的()组合驱动,ABSC方法是一个自项向下、递归细化的方法,软件系统的体系结构通过该方法细化,直到能产…...
IPv6网络实验:地址自动生成与全球单播通信探索
文章目录 一、实验背景与目的二、实验拓扑三、实验需求四、实验解法1. 在R1和PC3上开启IPv6链路本地地址自动生成,测试是否能够使用链路本地地址互通2. 为R1配置全球单播地址2001::1/64,使PC3能够自动生成与R1同一网段的IPv6地址3. 测试R1和PC3是否能够使…...
深入探索前端之道:JavaScript深拷贝与浅拷贝的解析与实现
引言 前端开发中,数据的复制是一个常见的操作。尤其是在处理对象和数组时,我们需要考虑的是一个浅拷贝还是深拷贝。那么,什么是深拷贝和浅拷贝?它们在前端开发中有什么作用?如何实现这两种拷贝?这是我们在…...
关于两个不同数据库的两张表建立数据库链接,关联查询数据
一、数据库链接 数据库链接(database link)是用于跨不同数据库之间进行连接和数据传输的工具或方法。它允许在一个数据库中访问另一个数据库中的对象和数据。 二、具体操作 以Oracle数据库为例 --1.建立链接tjpt CREATE DATABASE LINK tjpt CONNECT…...
Google登录SDK
一、接入的准备工作 官方文档链接地址:开始使用一键登录和注册 按照步骤进行接入即可 二、项目参考(Unity项目) 注意:代码版本如果不适用新的Google API 请自行参考最新版本接口 SDKGoogleSignInActivity 主要用于登录的代码。Un…...
ASP.NET Core 8 的运行环境 Environment
开发流程一般有3个阶段: 开发 Development测试 Stage正式 Production 运行时环境变量可以用于根据不同的开发阶段运行不同的逻辑,比如在开发阶段的某些功能或保密信息不暴露在正式上线的代码中。 在Visual Stduio创建的模板代码中是否为开发环境Envir…...
机械臂手眼标定ZED相机——眼在手外python、matlab
目录 1.眼在手外原理 2.附上眼在手外求得手眼矩阵的python代码 3.眼在手外标定步骤 1)打印棋盘格 2)得到hand数据 3)得到camera数据 4.运行python得到手眼矩阵 1.眼在手外原理 眼在手外所求的手眼矩阵是基坐标到相机的转换矩阵 2.附上…...
前端实现动态路由(前端控制全部路由,后端返回用户角色)
优缺点 优点: 不用后端帮助,路由表维护在前端逻辑相对比较简单,比较容易上手权限少的系统用前端鉴权更加方便 缺点: 线上版本每次修改权限页面,都需要重新打包项目大型项目不适用如果需要在页面中增加角色并且控制可以访问的页…...
Spring5学习笔记—Spring事务处理
✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉 🍎个人主页:Leo的博客 💞当前专栏: Spring专栏 ✨特色专栏: M…...
如何增长LLM推理token,从直觉到数学
背景: 最近大模型输入上文长度增长技术点的研究很火。为何要增长token长度,为何大家如此热衷于增长输入token的长度呢?其实你如果是大模型比价频繁的使用者,这个问题应该不难回答。增长了输入token的长度,那需要多次出入才能得到…...
《穷爸爸与富爸爸》时间是最宝贵的资产,只有它对所有人都是公平的
《穷爸爸与富爸爸》时间是最宝贵的资产,只有它对所有人都是公平的 罗伯特清崎,日裔美国人,投资家、教育家、企业家。 萧明 译 文章目录 《穷爸爸与富爸爸》时间是最宝贵的资产,只有它对所有人都是公平的[toc]摘录各阶层现金流图支…...
Git结合Gitee的企业开发模拟
本系列有两篇文章: 一是另外一篇《快速使用Git完整开发》,主要说明了关于Git工具的基础使用,包含三板斧(git add、git commit、git push)、Git基本配置、版本回退、分支管理、公钥与私钥、远端仓库和远端分支、忽略文…...
HTML 语义化
目录 HTML 语义化HTML5 新特性HTML 语义化的好处语义化标签的使用场景最佳实践 HTML 语义化 HTML5 新特性 标准答案: 语义化标签: <header>:页头<nav>:导航<main>:主要内容<article>&#x…...
C++:std::is_convertible
C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...
镜像里切换为普通用户
如果你登录远程虚拟机默认就是 root 用户,但你不希望用 root 权限运行 ns-3(这是对的,ns3 工具会拒绝 root),你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案:创建非 roo…...
苍穹外卖--缓存菜品
1.问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大 2.实现思路 通过Redis来缓存菜品数据,减少数据库查询操作。 缓存逻辑分析: ①每个分类下的菜品保持一份缓存数据…...
C++中string流知识详解和示例
一、概览与类体系 C 提供三种基于内存字符串的流,定义在 <sstream> 中: std::istringstream:输入流,从已有字符串中读取并解析。std::ostringstream:输出流,向内部缓冲区写入内容,最终取…...
【Linux】Linux 系统默认的目录及作用说明
博主介绍:✌全网粉丝23W,CSDN博客专家、Java领域优质创作者,掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域✌ 技术范围:SpringBoot、SpringCloud、Vue、SSM、HTML、Nodejs、Python、MySQL、PostgreSQL、大数据、物…...
脑机新手指南(七):OpenBCI_GUI:从环境搭建到数据可视化(上)
一、OpenBCI_GUI 项目概述 (一)项目背景与目标 OpenBCI 是一个开源的脑电信号采集硬件平台,其配套的 OpenBCI_GUI 则是专为该硬件设计的图形化界面工具。对于研究人员、开发者和学生而言,首次接触 OpenBCI 设备时,往…...
Proxmox Mail Gateway安装指南:从零开始配置高效邮件过滤系统
💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「storms…...
数学建模-滑翔伞伞翼面积的设计,运动状态计算和优化 !
我们考虑滑翔伞的伞翼面积设计问题以及运动状态描述。滑翔伞的性能主要取决于伞翼面积、气动特性以及飞行员的重量。我们的目标是建立数学模型来描述滑翔伞的运动状态,并优化伞翼面积的设计。 一、问题分析 滑翔伞在飞行过程中受到重力、升力和阻力的作用。升力和阻力与伞翼面…...
WebRTC调研
WebRTC是什么,为什么,如何使用 WebRTC有什么优势 WebRTC Architecture Amazon KVS WebRTC 其它厂商WebRTC 海康门禁WebRTC 海康门禁其他界面整理 威视通WebRTC 局域网 Google浏览器 Microsoft Edge 公网 RTSP RTMP NVR ONVIF SIP SRT WebRTC协…...
