速通STM32-基础篇-iic
本文介绍了I2C总线的基本原理及在STM32上的应用。文章详细解析了I2C的电路结构、数据传输机制(包括起始位、地址位、数据位、应答位等)及通信协议。同时提供了STM32中I2C模块的初始化、数据发送和接收的具体代码实现方法,包括等待总线空闲、发送起始位、寻址、数据传输等关键步骤。该文适合嵌入式初学者构建I2C知识框架,可作为STM32入门学习参考。
阅读小tips!(Look at my eyes!)
本篇基于电子类大学生自主学习嵌入式的学习总结,知识点理解输出较于浅显,只适用于深入学习stm32前的一些理解和引导,或是大学期末前一周速通,若要深入学习嵌入式,可借助阅读本篇来搭建一个入门嵌入式的框架,而后想深入可转战b站等学习网站,有更多的大牛在等待你!
一.揭开iic神秘面纱
IIC通常也写作I2C,是一种双向通信总线,对于初学者来说,词汇的过于专业化,还是不好理解,到底什么叫做通信总线呢?
不妨来这样理解,上篇我们共同探索了串口,(tips:没有看过的小伙伴可以移步至上一篇串口篇),我们对于串口还是比较熟悉,串口是一种通信接口,它让设备与设备之间能够通信,串口的TX和RX通常要占用一组IO口,基础的单片机如stm32f103c8t6只配备了三组串口,当我们只有三组通信接口,少量IO口的情况下,如果我们要进行多设备通信,又该如何呢?
单片机为我们提供了iic总线,这种接口通信,实现了一接口对多设备,大大解决了多设备通信的难题,也解放了多组IO口。
IIC总线的基础电路结构:

IIC通过SCL和SDA两条线建立主机(一般是单片机)与从机(其他设备或单片机)之间的通信。
从机的地址:由7的bit位组成,0000 000 - 1111 111,1byte最后一个bit位0/1代表主机读/写数据。
时钟线与数据线:

SCL:
时钟线,一般由主机发送时钟信号,在时钟信号的每一个周期中,数据线上发送一位数据。
传输频率的快慢决定了数据的传输速度。
SDA:
数据线,传输双向,可以由主机传给从机,也可以由从机传给主机,数据位的高低电平决定了0/1.
逻辑线与:
与运算:Y = 1&1&...1 = 1 Y = 1&1&...0 = 0
tips:所有的主机从机连接SDA和SCL引脚都设置为了开漏输出,所以当引脚为高电平时保持高阻态,逻辑上释放总线,由上拉电阻拉高,有引脚为低电平时接入GND。
上拉电阻:
当SDA和SCL线上为高阻态时,将引脚拉为高电平。
数据传输:

主机发送时钟信号:
所有从机连接SCL的引脚保持为高电平,逻辑上释放总线,主机连接SCL的引脚通过写1/0,实现高低电平的转换,发送一个时钟脉冲信号,写0为低电平写1为高电平。
主机发送数据信号:
与发送时钟信号相同,所有从机连接SDA的引脚保持为高电平,逻辑上释放总线,主机连接SDA的引脚通过写1/0,实现高低电平的转换,发送一个脉冲信号。
从机发送数据信号:
由于I2C通信是双向的,从机接收数据信号的同时也能发送数据信号,与主机发送数据相同,主机与其余从机连接SDA的引脚保持为高电平,逻辑上释放总线,选中的从机连接SDA的引脚通过写1/0,实现高低电平的转换,发送一个脉冲信号。
I2C通信协议:
数据帧格式:

起始位与停止位:
空闲状态下保持高电平,当数据传输开始时,由主机拉低发送开始信号,数据传输结束时,由主机拉高释放总线。
RW位:
7位地址为从机地址,R表示读数据写0,W表示写数据写1。
ACK与NAK:
ACK:标志位,0-发送NAK,1-发送ACK。只作用于正在被接受的字节。
应答信号,当主机发送一个字节结束时,主机释放SDA总线,从机发送ACK应答信号,拉低SDA总线,表示成功接收数据,并为下一个字节传输做准备。当从机不想接受数据时,主机释放SDA总线,从机不去拉低,发送NAK应答信号,表示数据传输结束,主机紧接着发送停止位。
二、IIC模块的使用
iic的速度模式选择:

一般的IIC通信,选择快速模式。
时钟信号的占空比:

一般默认选择2/1的时钟信号占空比。
IIC模块的初始化:
此处默认已初始化过IO口。
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE);//开启I2C1时RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1,ENABLE);//施加复位信号RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1,DISABLE);//释放复位信号
I2C_InitTypeDef I2C_InitStruct;
I2C_InitStruct.I2C_ClockSpeed = 400000;//波特率400K
I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;//标准的I2C
I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;//占空比2:1
I2C_Init(I2C1,&I2C_InitStruct);
I2C_Cmd(I2C1,ENABLE);//闭合I2C总开关
I2C写数据:
IIC模块的内部结构框图:

SR:Status Register 状态寄存器,用来储存标志位。接下来将在编写代码过程中逐一对标志位进行讲解。TxE RxNE 串口章节已详细讲解,本章不做赘述。
定义发送数据函数:
int My_I2C_SendBytes(I2C_TypeDef * I2Cx,//I2C接口名称
uint8_t Addr,//从机地址,靠左
uint8_t * pData,//要发送的数据
uint16_t Size//要发送的数据的数量 字节);
通过I2C向从机发送若干个字节。返回值:0-成功 1-寻址失败 2-发送的数据被拒绝
等待总线空闲:
BUSY -总线忙标志位,0-总线空闲 1-总线忙。
//等待总线空闲
while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_BUSY)== SET);
发送起始位:
START - 发送起始位,向该比特位写1发送起始位。
SB - 起始位发送完成,0-起始位未发送,1-起始位发送完成。
//发送起始位
I2C_GenerateSTART(I2Cx,ENABLE);
while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_SB == RESET));
发送地址:
AF- 应答失败 1- 未收到ACK
ADDR - 寻址成功 1-寻址成功
//寻址阶段
I2C_ClearFlag(I2Cx,I2C_FLAG_AF);
I2C_SendData(I2Cx,Addr & 0xfe);//左对齐,最后一位为R/W位
while(1)
{
if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_ADDR) == SET)
break;
if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_AF) == SET)
{
I2C_GenerateSTOP(I2Cx,ENABLE);
return -1;
}
//清空ADDR
I2C_ReadRegister(I2Cx,I2C_Register_SR1);
I2C_ReadRegister(I2Cx,I2C_Register_SR2);
}
发送数据:
//发送数据
for(uint16_t i=0;i<size;i++)
{
while(1)
{
if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_AF)==SET)
{
I2C_GenerateSTOP(I2Cx,ENABLE);
return -2;
}
if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_TXE)==SET)
break;
}
I2C_SendData(I2Cx,pData[i]);
}
BTF -数据发送完成标志位,1-数据发送完成
//检查数据是否发送完成
while(1)
{
if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_AF) == SET)
{
I2C_GenerateSTOP(I2Cx,ENABLE);
return -2;
}
if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_BTF) == SET)
break;
}
发送停止位:
STOP- 发停止位,向该比特位写1发送停止位。
//发送停止位
I2C_GenerateSTOP(I2Cx,ENABLE);
return 0;
定义接收数据函数:
int My_I2C_RecieveBytes(I2C_TypeDef * I2Cx,//I2C接口名称
uint8_t Addr,//从机地址,靠左
uint8_t * pBuffer,//要接收的数据
uint16_t Size//要接收的数据的数量 字节);
通过I2C向从机接收若干个字节。返回值:0-成功 1-寻址失败 2-发送的数据被拒绝
发送起始位:
//发送起始位
I2C_GenerateSTART(I2Cx,ENABLE);
while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_SB == RESET));
发送地址:
//寻址阶段
I2C_ClearFlag(I2Cx,I2C_FLAG_AF);
I2C_SendData(I2Cx,Addr | 0x01);//左对齐,最后一位为R/W位
while(1)
{
if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_ADDR) == SET)
break;
if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_AF) == SET)
{
I2C_GenerateSTOP(I2Cx,ENABLE);
return -1;
}
}
接收数据:
//接收数据
if(Size == 1)
{
//清空ADDR
I2C_ReadRegister(I2Cx,I2C_Register_SR1);
I2C_ReadRegister(I2Cx,I2C_Register_SR2);
//ACK =0 STOP =1
I2C_AcknowledgeConfig(I2Cx,DISABLE);
I2C_GenerateSTOP(I2Cx,ENABLE);
// 等待接收完成
while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
//读取数据
pBuffer[0] = I2C_ReceiveData(I2Cx);
}
else if(Size == 2)
{
//清空ADDR
I2C_ReadRegister(I2Cx,I2C_Register_SR1);
I2C_ReadRegister(I2Cx,I2C_Register_SR2);
//ACK=1
I2C_AcknowledgeConfig(I2Cx,ENABLE);
// 等待接收完成
while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
//读取第一个数据
pBuffer[0] = I2C_ReceiveData(I2Cx);
//ACK =0 STOP =1
I2C_AcknowledgeConfig(I2Cx,DISABLE);
I2C_GenerateSTOP(I2Cx,ENABLE);
// 等待接收完成
while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
//读取第二个数据
pBuffer[1] = I2C_ReceiveData(I2Cx);
}
else //读取多个字节
{
//清空ADDR
I2C_ReadRegister(I2Cx,I2C_Register_SR1);
I2C_ReadRegister(I2Cx,I2C_Register_SR2);
//ACK=1
I2C_AcknowledgeConfig(I2Cx,ENABLE);
for(uint16_t i=0;i<sizeof-1;i++)
{
// 等待接收完成
while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
//读取数据
pBuffer[i] = I2C_ReceiveData(I2Cx);
}
//ACK =0 STOP =1
I2C_AcknowledgeConfig(I2Cx,DISABLE);
I2C_GenerateSTOP(I2Cx,ENABLE);
// 等待接收完成
while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
//读取最后一个数据
pBuffer[1] = I2C_ReceiveData(I2Cx);
}
发送停止位:
//发送停止位
//I2C_GenerateSTOP(I2Cx,ENABLE);接收数据中已写
return 0;
创作不易,请多多点赞,收藏,转发,您的支持,是我创作最大的动力!
内容若有误,在所难免,还望指出,定会积极订正,不胜感激,还望海涵!
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐

所有评论(0)