
IIC(Inter Integrated Circuit)总线在物理层由SDA(Serial data, 串行数据线)、SCL(Serial clock line,串行时钟线)和上拉电阻组成。
高阻态:高阻状态是三态门电路的一种状态。逻辑门的输出除有高、低电平两种状态外,还有第三种状态——高阻状态的门电路。电路分析时高阻态可做开路理解。
漏极开路/集电极开路即为高阻态,若需要产生高电平,则需使用外部上拉电阻。
SDA 和SCL通过一个电流源或上拉电阻连接到正的电源电压,当总线空闲时,这两条线都是高电平,内部电平如下图所示:

相关术语:

IIC协议标准规定发起通信(控制时钟线,即控制SCL的电平高低变换)的设备称为主设备,主设备发起一次通信后,其它设备均为从设备。
发送器与接收器的角色,与主机和从机没有关系:


/**
* @brief I2C起始信号
* SDA -> Output
*/
void I2CStart(void)
{
SDA = 1; // 确保SCL拉高前SDA为高电平
delay_us(5);
SCL = 1;
delay_us(5);
SDA = 0;
delay_us(5);
SCL = 0; // SCL变低,起始信号结束
delay_us(5);
}
/**
* @brief I2C停止信号
* SDA -> Output
*/
void I2CStop(void)
{
SCL = 0;
SDA = 0;
delay_us(5);
SCL = 1; // SCL拉高,释放总线控制权,停止接收数据
SDA = 1;
delay_us(5);
}

主机发起通讯时(产生起始信号),首先会通过SDA线发送设备地址(7位或10位)来查找从机,LSB位用来表示数据传输方向:
只有当SCL为高电平时才能传输数据,且SDA线上的数据必须保持稳定(不允许高低跳变),只有当SCL为低时,SDA线上的数据才允许切换状态。
数据传输时先传输MSB,输出到SDA线上的数据必须为8位(传输设备地址时,7位地址+1位表示读/写),且每个字节后面必须紧跟一位应答位(即一帧数据9位)
主机确定了从机的设备地址后,生成一个开始信号,然后向IIC总线上面发送设备的地址和读写方向标志。从机检测到该地址和自己设备地址相对应后,回复主机一个应答信号。主机接收到应答信号后就开始向这个设备以字节为单位发送数据,每一个字节后面都会带有从机的应答信号,直到主机发送完成最后一个数据后生成一个停止信号结束此次数据的传输。


读操作与写操作类似。
下面为发送/接收8位数据,不包括起始/停止条件:
/**
* @brief 发送字节数据
* SDA -> Output
*/
void I2CSendByte(uint8_t data)
{
uint8_t i = 8;
while (i--)
{
SCL = 0;
delay_us(2);
SDA = !!(data & 0x80); // 将数据转换为bool型
data <<= 1;
delay_us(2);
SCL = 1;
delay_us(2);
}
SCL = 0;
delay_us(2);
}
/**
* @brief 读取字节数据
* SDA -> Input
*/
uint8_t I2CReadByte(uint8_t ack)
{
uint8_t i = 8, data = 0;
SDA_Input_Mode();
while (i--)
{
SCL = 0;
delay_us(2);
SCL = 1;
delay_us(1);
data <<= 1; // 第一次右移为0,然后依次移位7次
data |= SDA;
}
SDA_Output_Mode();
if (ack)
I2CSendACK(); // 发送应答
else
I2CSendNACK(); // 发送非应答
return data;
}

当数据发送端传送8位数据结束后,在第9个时钟时,数据发送端会将SDA线拉高(释放SDA的控制权),防止数据冲突,由数据接收端控制SDA,此时:
NACK),说明数据接收端已成功地接收了该字节ACK),说明数据接收端接收该字节未成功/**
* @brief 发送应答信号
* SDA -> Output
*/
void I2CSendACK(void)
{
SCL = 0;
SDA = 0; //拉低SDA,产生应答信号
delay_us(2);
SCL = 1;
delay_us(5);
SCL = 0;
}
/**
* @brief 发送非应答信号
* SDA -> Output
*/
void I2CSendNACK(void)
{
SCL = 0;
SDA = 1; //拉高SDA,不产生应答信号
delay_us(2);
SCL = 1;
delay_us(5);
SCL = 0;
}
当 发送器 需要等待并接收 接收器 的应答信号时,需要将发生器SDA数据线由输出模式修改为输入模式:
/**
* @brief 等待应答信号
* SDA -> Input
*/
int I2CSendACK(void)
{
uint8_t timeout = 5;
SDA_Input_Mode(); // 将主机SDA引脚GPIO变为输入模式
delay_us(2);
SCL = 1;
delay_us(2);
while (SDA) // 读取SDA总线电平, 若接收器应答则会低电平退出循环;否则将超时错误返回
{
timeout--;
delay_us(1);
if (0 == timeout)
{
SDA_Output_Mode(); // 将主机SDA引脚GPIO变为输出模式
I2CStop();
return ERROR;
}
}
SDA_Output_Mode();
SCL = 0;
delay_us(2);
return SUCCESS;
}

IIC的仲裁机制得益于其开漏的输入输出结构。当SCL线上挂载的多个设备,其中的MCU2的SCL输出低电平,那么这条IIC总线SCL就会被MCU2拉低,体现线与特性。
如下图所示,CLK1和CLK2都是连接在一条SCL线上的设备同时产生的时钟信号,由于IIC总线存在“线与”的特性,同为高电平才能输出高电平,有1个为低电平则全部为低电平,因此同一条SCL总线上面的时钟都是相同的。

由此可知:产生的同步SCL 时钟的低电平周期由低电平时钟周期最长的器件决定,而高电平周期由高电平时钟周期最短的器件决定。
SDA仲裁也是基于“线与”的特性,因为SCL高电平时才能传输数据,所以SCL高电平期间开始仲裁。
下图显示了两个主机的仲裁过程,在第1个和第2个周期内DATA1和DATA2的数据都是相同的,当在第2个时钟周期时DATA1与SDA的数据不一致,这个时候设备1就会停止发送数据,转而启动接收模式(变为高电平)。这样SDA的数据就会与DATA2的数据保持一致,并且设备1停止发送数据也不会影响SDA的数据。

注意:在串行传输时当重复起始条件或停止条件发送到I2C 总线的时侯仲裁过程仍在进行,即一帧数据完全相同,此时相关主机必须发送重复起始条件或停止条件,因此仲裁在不能下面情况进行:
从机不参与上述仲裁。



时间配置:
I2C->TIMINGR bit[23:20] SCLDEL[3:0]位)I2C->TIMINGR bit[19:16] SDADEL[3:0]位) — 需关闭时钟拉伸才生效I2C->CR1的ANFOFF位使能/禁用模拟噪声滤波器,通过I2C->CR1的DNF[3:0]位选择数字滤波器系数。ANFOFF位为0表示使能从机配置:
I2C->CR1 bit17 NOSTRETCH位),clock stretching通过将SCL线拉低来暂停一个传输,直到释放SCL线为高电平,传输才继续进行,一般不用。HAL函数模型有轮询、中断、DMA三种,下面仅分析轮询模式函数
/**
* @brief 在主机模式下以阻塞模式传输数据
* @param hi2c
* @param DevAddress 目标设备地址: 7位地址, 必须向左移1位!!!
* @param pData 指针数据缓冲区的指针(写入过程中指针会根据字字数自增)
* @param Size 要发送数据的字节数
* @param Timeout 超时时间
* @retval HAL status
*/
HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData,
uint16_t Size, uint32_t Timeout)
/**
* @brief 在主机模式下以阻塞模式接收数据
* @param hi2c
* @param DevAddress 目标设备地址: 7位地址, 必须向左移1位!!!
* @param pData 指针数据缓冲区的指针(读取过程中指针会根据字字数自增)
* @param Size 要接收数据的字节数
* @param Timeout 超时时间
* @retval HAL status
*/
HAL_StatusTypeDef HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData,
uint16_t Size, uint32_t Timeout)
/**
* @brief 在从机模式下以阻塞模式接收数据
* @param hi2c
* @param pData 指针数据缓冲区的指针
* @param Size 要接收数据的字节数
* @param Timeout 超时时间
* @retval HAL status
*/
HAL_StatusTypeDef HAL_I2C_Slave_Transmit(I2C_HandleTypeDef *hi2c, uint8_t *pData, uint16_t Size,
uint32_t Timeout)
HAL_I2C_Slave_Receive读数据与之类似。和主机相比,少了设备地址参数。
/**
* @brief 在阻塞模式下向从机特定的内存地址写/读入数据
* @param hi2c
* @param DevAddress 目标设备地址: 7位地址, 必须向左移1位!!!
* @param MemAddress 从机寄存器地址(写入过程中会自加)
* @param MemAddSize 从机寄存器地址的大小(8位或16位)
* @param pData 指针数据缓冲区的指针
* @param Size 要接收数据的字节数
* @param Timeout 超时时间
* @retval HAL status
*/
HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress,
uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout)
该函数适用于IIC外设里面还有子地址寄存器的设备,如AT24CXX E2PROM存储器,除了设备地址,每个存储字节都有其对应的地址。
其中MemAddSize可选宏:
#define I2C_MEMADD_SIZE_8BIT (0x00000001U)
#define I2C_MEMADD_SIZE_16BIT (0x00000002U)
使用**HAL_I2C_Mem_Write等于先使用HAL_I2C_Master_Transmit传输第一个寄存器地址,再用HAL_I2C_Master_Transmit**传输写入第一个寄存器的数据:
uint8_t Cmd_Code[2] = {0x00, 0x00};
uint8_t Data_Code[2] = {0x40, 0x00};
extern I2C_HandleTypeDef hi2c3;
void OLED_Write(uint8_t type, uint8_t data)
{
if (type == TYPE_COMMAND)
{
Cmd_Code[1] = data;
// HAL_I2C_Master_Transmit(&hi2c3, 0x78, Cmd_Code, 2, 100);
HAL_I2C_Mem_Write(&hi2c3, 0x78, 0x00, I2C_MEMADD_SIZE_8BIT, &data, 1, 100);
}
else
{
Data_Code[1] = data;
// HAL_I2C_Master_Transmit(&hi2c3, 0x78, Data_Code, 2, 100);
HAL_I2C_Mem_Write(&hi2c3, 0x78, 0x40, I2C_MEMADD_SIZE_8BIT, &data, 1, 100);
}
}
HAL_I2C_Mem_Read读数据与写数据函数类似。
参考:
END
我想在一个没有Sass引擎的类中使用Sass颜色函数。我已经在项目中使用了sassgem,所以我认为搭载会像以下一样简单:classRectangleincludeSass::Script::FunctionsdefcolorSass::Script::Color.new([0x82,0x39,0x06])enddefrender#hamlengineexecutedwithcontextofself#sothatwithintemlateicouldcall#%stop{offset:'0%',stop:{color:lighten(color)}}endend更新:参见上面的#re
我正在尝试用ruby中的gsub函数替换字符串中的某些单词,但有时效果很好,在某些情况下会出现此错误?这种格式有什么问题吗NoMethodError(undefinedmethod`gsub!'fornil:NilClass):模型.rbclassTest"replacethisID1",WAY=>"replacethisID2andID3",DELTA=>"replacethisID4"}end另一个模型.rbclassCheck 最佳答案 啊,我找到了!gsub!是一个非常奇怪的方法。首先,它替换了字符串,所以它实际上修改了
我有一些代码在几个不同的位置之一运行:作为具有调试输出的命令行工具,作为不接受任何输出的更大程序的一部分,以及在Rails环境中。有时我需要根据代码的位置对代码进行细微的更改,我意识到以下样式似乎可行:print"Testingnestedfunctionsdefined\n"CLI=trueifCLIdeftest_printprint"CommandLineVersion\n"endelsedeftest_printprint"ReleaseVersion\n"endendtest_print()这导致:TestingnestedfunctionsdefinedCommandLin
如何在Ruby中按名称传递函数?(我使用Ruby才几个小时,所以我还在想办法。)nums=[1,2,3,4]#Thisworks,butismoreverbosethanI'dlikenums.eachdo|i|putsiend#InJS,Icouldjustdosomethinglike:#nums.forEach(console.log)#InF#,itwouldbesomethinglike:#List.iternums(printf"%A")#InRuby,IwishIcoulddosomethinglike:nums.eachputs在Ruby中能不能做到类似的简洁?我可以只
文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,
说在前面这部分我本来是合为一篇来写的,因为目的是一样的,都是通过独立按键来控制LED闪灭本质上是起到开关的作用,即调用函数和中断函数。但是写一篇太累了,我还是决定分为两篇写,这篇是调用函数篇。在本篇中你主要看到这些东西!!!1.调用函数的方法(主要讲语法和格式)2.独立按键如何控制LED亮灭3.程序中的一些细节(软件消抖等)1.调用函数的方法思路还是比较清晰地,就是通过按下按键来控制LED闪灭,即每按下一次,LED取反一次。重要的是,把按键与LED联系在一起。我打算用K1来作为开关,看了一下开发板原理图,K1连接的是单片机的P31口,当按下K1时,P31是与GND相连的,也就是说,当我按下去时
最近在学习CAN,记录一下,也供大家参考交流。推荐几个我觉得很好的CAN学习,本文也是在看了他们的好文之后做的笔记首先是瑞萨的CAN入门,真的通透;秀!靠这篇我竟然2天理解了CAN协议!实战STM32F4CAN!原文链接:https://blog.csdn.net/XiaoXiaoPengBo/article/details/116206252CAN详解(小白教程)原文链接:https://blog.csdn.net/xwwwj/article/details/105372234一篇易懂的CAN通讯协议指南1一篇易懂的CAN通讯协议指南1-知乎(zhihu.com)视频推荐CAN总线个人知识总
我需要一个通过输入字符串进行计算的方法,像这样function="(a/b)*100"a=25b=50function.something>>50有什么方法吗? 最佳答案 您可以使用instance_eval:function="(a/b)*100"a=25.0b=50instance_evalfunction#=>50.0请注意,使用eval本质上是不安全的,尤其是当您使用外部输入时,因为它可能包含注入(inject)的恶意代码。另请注意,a设置为25.0而不是25,因为如果它是整数a/b将导致0(整数)。
我需要从json记录中获取一些值并像下面这样提取curr_json_doc['title']['genre'].map{|s|s['name']}.join(',')但对于某些记录,curr_json_doc['title']['genre']可以为空。所以我想对map和join()使用try函数。我试过如下curr_json_doc['title']['genre'].try(:map,{|s|s['name']}).try(:join,(','))但是没用。 最佳答案 你没有正确传递block。block被传递给参数括号外的方法
在这段Ruby代码中:ModuleMClassC当我尝试运行时出现“'M:Module'的未定义方法'helper'”错误c=M::C.new("world")c.work但直接从另一个类调用M::helper("world")工作正常。类不能调用在定义它们的同一模块中定义的模块函数吗?除了将类移出模块外,还有其他解决方法吗? 最佳答案 为了调用M::helper,你需要将它定义为defself.helper;结束为了进行比较,请查看以下修改后的代码段中的helper和helper2moduleMclassC