stm32 hal 模拟i2c
soft_i2c_WriteReg通过调用soft_i2c_WriteBytes,实现写入寄存器的功能,soft_i2c_WriteBytes发送的第一个内容是从机地址,第二个寄存器地址,第三四个内容是数据内容;先看这个结构体,这个结构体是软件i2c描述,里面包含了对sda线的读写,对scl线的写,以及每个操作的延时时间,延时函数指针,以及从机地址。代码如上,soft_i2c_Send 函数实现了
之前一篇文章里用了模拟i2c,我这边给大家介绍一下
模拟i2c本质上就是用gpio开关来模拟i2c协议,当然,i2c协议本质上也就是电平的高低
在理解如何使用软件模拟i2c前,你需要理解i2c协议,网上又很多相关文章,我也就不在多说
模拟软件i2c需要实现以下步骤
1.gpio引脚需要是开漏输出,因为只有开漏输出,从机设备才能应答,开漏输出具有“线与”的性质
2.实现i2c协议,i2c协议是由起始型号,终止信号,应答信号,数据信号来构成的。
第一步没什么好说的,我使用的cubemx进行配置,无论是固件库还是hal库,配置起来都不难
第二步要分成很多步,我们慢慢来看
首先看一个宏定义和一个结构体
typedef struct i2c_bit_ops
{
void (*set_sda)(int32_t state);
void (*set_scl)(int32_t state);
int32_t (*get_sda)(void);
void (*udelay)(uint32_t us);
uint32_t delay_us; /* scl and sda line delay */
uint8_t writeAddr;
}soft_i2c_driver;
先看这个结构体,这个结构体是软件i2c描述,里面包含了对sda线的读写,对scl线的写,以及每个操作的延时时间,延时函数指针,以及从机地址。
#define soft_i2c_Delay() i2c_Delay(i2c_driver)
#define soft_i2c_SDA_H() i2c_SDA_H(i2c_driver)
#define soft_i2c_SDA_L() i2c_SDA_L(i2c_driver)
#define soft_i2c_SCL_H() i2c_SCL_H(i2c_driver)
#define soft_i2c_SCL_L() i2c_SCL_L(i2c_driver)
#define soft_i2c_SDA_Read() i2c_SDA_Read(i2c_driver)
#define soft_i2c_Start() soft_i2c_start(i2c_driver)
#define soft_i2c_Stop() soft_i2c_stop(i2c_driver)
static void i2c_DelayUs(soft_i2c_driver *i2c_driver,uint16_t time)
{
i2c_driver->udelay(time);
}
static void i2c_Delay(soft_i2c_driver *i2c_driver)
{
i2c_DelayUs(i2c_driver,i2c_driver->delay_us);
}
static void i2c_SCL_H(soft_i2c_driver *i2c_driver)
{
i2c_driver->set_scl(soft_i2c_high);
}
static void i2c_SCL_L(soft_i2c_driver *i2c_driver)
{
i2c_driver->set_scl(soft_i2c_low);
}
static void i2c_SDA_H(soft_i2c_driver *i2c_driver)
{
i2c_driver->set_sda(soft_i2c_high);
}
static void i2c_SDA_L(soft_i2c_driver *i2c_driver)
{
i2c_driver->set_sda(soft_i2c_low);
}
static int32_t i2c_SDA_Read(soft_i2c_driver *i2c_driver)
{
uint8_t data;
data = i2c_driver->get_sda();
return data;
}
以上内容通过宏定义调用了结构,简化函数,为后面做铺垫
实现起始信号,起始信号应该是在SCL为高电平时,SDA拉低,表示起始信号
void soft_i2c_start(soft_i2c_driver *i2c_driver)
{
soft_i2c_SDA_H();
soft_i2c_SCL_H();
soft_i2c_SDA_L();
soft_i2c_Delay();
soft_i2c_SCL_L();
}
实现终止信号,终止信号实在SCL为高电平时,SDA拉高
void soft_i2c_stop(soft_i2c_driver *i2c_driver)
{
soft_i2c_SDA_L();
soft_i2c_SCL_H();
soft_i2c_SDA_H();
soft_i2c_Delay();
soft_i2c_SCL_L();
}
实现应答信号,应答信号是在一个SCL周期内,拉低SDA
static void i2c_ACK(soft_i2c_driver *i2c_driver)
{
soft_i2c_SDA_L();
soft_i2c_SCL_H();
soft_i2c_Delay();
soft_i2c_SCL_L();
}
除了应答信号,应答从机的回复,还要有等待应答的代码,用于等待从机的应答
uint8_t soft_i2c_waitAck(soft_i2c_driver *i2c_driver)
{
uint8_t ack;
soft_i2c_SDA_H();
soft_i2c_SCL_H();
soft_i2c_Delay();
ack = soft_i2c_SDA_Read();
soft_i2c_SCL_L();
return ack;
}
上面就是模拟i2c的基础,后续需要实现i2c的读写时序
先说i2c写时序,这部分内容相对简单,下图来自于网络

该时序图的流程为
1.主机发送START信号
2.主机发送七位从机地址和一位读写位(写位为0)
3.等待从机返回ACK指令
4.发送寄存器地址
5.等待从机返回ACK指令
6.发送写入数据
7.等待从机返回ACK指令
8.发送STOP信号
void soft_i2c_Send(soft_i2c_driver *i2c_driver,uint8_t data)
{
int i=0;
for(i=0;i<8;i++)
{
soft_i2c_SCL_L();
if(data & 0x80)
{
soft_i2c_SDA_H();
}
else
{
soft_i2c_SDA_L();
}
soft_i2c_Delay();
soft_i2c_SCL_H();
data = data << 1;
}
soft_i2c_SCL_L();
}
uint8_t soft_i2c_WriteBytes(soft_i2c_driver *i2c_driver,uint8_t data[],uint8_t size)
{
int i=0;
soft_i2c_start(i2c_driver);
for(i=0;i<size;i++)
{
soft_i2c_Send(i2c_driver,data[i]);
if(soft_i2c_waitAck(i2c_driver))
{
return 0;
}
}
soft_i2c_stop(i2c_driver);
return 1;
}
uint8_t soft_i2c_WriteReg(soft_i2c_driver *i2c_driver, uint8_t reg, uint8_t reg_msb, uint8_t reg_lsb)
{
uint8_t data[4];
data[0] = i2c_driver->writeAddr;
data[1] = reg;
data[2] = reg_msb;
data[3] = reg_lsb;
return soft_i2c_WriteBytes(i2c_driver,data,4);
}
代码如上,soft_i2c_Send 函数实现了发送的逻辑,soft_i2c_WriteBytes实现了发送的完整逻辑,
soft_i2c_WriteReg通过调用soft_i2c_WriteBytes,实现写入寄存器的功能,soft_i2c_WriteBytes发送的第一个内容是从机地址,第二个寄存器地址,第三四个内容是数据内容;
接着我们来讲读数据的时序

读数据比写数据麻烦些
1.主机发送START信号
2.主机发送从机地址和写位(0)
3.等待从机返回ACK指令
4.主机发送寄存器地址
5.等待从机返回ACK指令
6.主机发送STOP信号,在发送START信号
7.主机发送从机地址
8.等待从机返回ACK指令
9.主机读取从机发送的数据
10.发送STOP信号
uint8_t soft_i2c_Receive(soft_i2c_driver *i2c_driver)
{
int i=0;
uint8_t data;
soft_i2c_SDA_H();
for(i=0;i<8;i++)
{
data = data << 1;
soft_i2c_SCL_H();
soft_i2c_Delay();
data = data + soft_i2c_SDA_Read();
soft_i2c_SCL_L();
}
i2c_ACK(i2c_driver);
return data;
}
uint8_t soft_i2c_ReadBytes(soft_i2c_driver *i2c_driver,uint8_t reg, uint8_t data[],uint8_t size)
{
soft_i2c_start(i2c_driver);
soft_i2c_Send(i2c_driver,i2c_driver->writeAddr);
while (soft_i2c_waitAck(i2c_driver));
soft_i2c_Send(i2c_driver,reg);
while (soft_i2c_waitAck(i2c_driver));
soft_i2c_start(i2c_driver);
soft_i2c_Send(i2c_driver,i2c_driver->writeAddr + 1);
while (soft_i2c_waitAck(i2c_driver));
for(uint8_t i=0;i<size;i++)
{
data[i] = soft_i2c_Receive(i2c_driver);
}
soft_i2c_stop(i2c_driver);
return 1;
}
uint16_t soft_i2c_ReadReg(soft_i2c_driver *i2c_driver,uint8_t reg)
{
uint8_t data[2];
soft_i2c_ReadBytes(i2c_driver,reg,data,2);
return data[0]<<8 | data[1];
}
soft_i2c_Receive实现你读数据的部分的逻辑,soft_i2c_ReadBytes实现了完整的读时序,soft_i2c_ReadReg读取寄存器中两个字节。
以上就是软件i2c的基础代码,后续只要实现对应接口的读写,填充从机地址和等待时序时间即可
这部分代码是参考了rt-thread的软件i2c代码,原来还觉得的自己写的挺完美的,但是回过头看来还是又不少问题,希望读者注意
1.这边需要填写的从机地址是7位地址左移一位的地址,使用的时候需要注意
2.这边的soft_i2c_WriteReg 和 soft_i2c_ReadReg 写的过于局限,或许拓展一下,可以多个字节,或者写多个字节会更加合理
3.这个问题最严重,soft_i2c_ReadBytes中的读数据使用的while来进行等待ACK,十分的不合理,合理的做法应该时等待一段时间后查看ack数据,之前这么写时十分不严谨的,望各位读者注意。
最后我再给大家一个完善的soft_i2c 初始化的样例,供大家参考
#include "gpio.h"
#include "main.h"
#include "soft_i2c.h"
//define soft I2c PORT PIN
#define _I2c1_SDA_PORT SDA_1_GPIO_Port
#define _I2c1_SDA_PIN SDA_1_Pin
#define _I2c1_SCL_PORT SCL_1_GPIO_Port
#define _I2c1_SCL_PIN SCL_1_Pin
//soft i2c define
soft_i2c_driver soft_i2c1;
soft_i2c_driver soft_i2c2;
static void delay_us(uint32_t time)
{
uint32_t delay_count = time * 168;
while(delay_count--)
{
}
}
//define i2c1 function
void set_sda_i2c1(int32_t state)
{
HAL_GPIO_WritePin(_I2c1_SDA_PORT, _I2c1_SDA_PIN, (GPIO_PinState)state);
}
void set_scl_i2c1(int32_t state)
{
HAL_GPIO_WritePin(_I2c1_SCL_PORT, _I2c1_SCL_PIN, (GPIO_PinState)state);
}
int32_t get_sda_i2c1(void)
{
return HAL_GPIO_ReadPin(_I2c1_SDA_PORT, _I2c1_SDA_PIN);
}
//init soft i2c
void driver_i2c_init(void)
{
soft_i2c_init(&soft_i2c1,(0x90),set_sda_i2c1,set_scl_i2c1,get_sda_i2c1,delay_us,2);
soft_i2c_init(&soft_i2c2,(0x92),set_sda_i2c1,set_scl_i2c1,get_sda_i2c1,delay_us,2);
}
以上代码时通过通过同一个软件i2c,通过设置不同的的从机地址,初始化了两个软件i2c,delay_us函数通过空转特定的指令数量来实现
以上就是soft_i2c的基本内容,请十分注意的上面说的缺陷,尤其是第三点,后续我也会再进行一版更新,解决这个问题
-------------------------------------------------------------------------------------------------------------------------------
以下是正对于Soft_i2c的更新
此次更新针对于第二点和第三点进行了更新,并且修复了一个bug,优化了i2c时序
1.提供了多字节读出的函数
2.添加ACK超时判断
3.修复之间读取多个字节时没有反馈Nack导致的时序错误问题
4.优化i2c波形,使scl间隔事件固定
第一点更新代码如下,如果需要读取多个字节,直接调用soft_i2c_ReadBytes即可,但如果需要写入多个字节,目前还不支持直接调用soft_i2c_WriteBytes,但提供了soft_i2c_WriteReg_16Bits,方便一次性写入两个字节,主要目前没有遇到需要写入多个字节的情况,后续会将该接口统一,修改为和soft_i2c_ReadBytes一样可以直接读取多个字节,而不需要填充从机地址
uint8_t soft_i2c_WriteBytes(soft_i2c_driver *i2c_driver,uint8_t data[],uint8_t size)
{
int i=0;
soft_i2c_start(i2c_driver);
for(i=0;i<size;i++)
{
soft_i2c_Send(i2c_driver,data[i]);
if(soft_i2c_waitAck(i2c_driver))
{
return 0;
}
}
soft_i2c_stop(i2c_driver);
return 1;
}
uint8_t soft_i2c_WriteReg_8Bits(soft_i2c_driver *i2c_driver, uint8_t reg, uint8_t reg_data)
{
uint8_t data[4];
data[0] = i2c_driver->writeAddr;
data[1] = reg;
data[2] = reg_data;
return soft_i2c_WriteBytes(i2c_driver,data,3);
}
uint8_t soft_i2c_WriteReg_16Bits(soft_i2c_driver *i2c_driver, uint8_t reg, uint16_t reg_data)
{
uint8_t data[4];
data[0] = i2c_driver->writeAddr;
data[1] = reg;
data[2] = (reg_data >> 8) & 0xFF;
data[3] = reg_data & 0xFF;
return soft_i2c_WriteBytes(i2c_driver,data,4);
}
uint8_t soft_i2c_ReadBytes(soft_i2c_driver *i2c_driver,uint8_t reg, uint8_t data[],uint8_t size)
{
uint32_t tick;
soft_i2c_start(i2c_driver);
soft_i2c_Send(i2c_driver,i2c_driver->writeAddr);
if(soft_i2c_waitAckTimeOut(i2c_driver))
{
return 0;
}
soft_i2c_Send(i2c_driver,reg);
if(soft_i2c_waitAckTimeOut(i2c_driver))
{
return 0;
}
soft_i2c_start(i2c_driver);
soft_i2c_Send(i2c_driver,i2c_driver->writeAddr + 1);
if(soft_i2c_waitAckTimeOut(i2c_driver))
{
return 0;
}
for(uint8_t i=0;i<size;i++)
{
data[i] = soft_i2c_Receive(i2c_driver);
if(i+1< size)
{
i2c_ACK(i2c_driver);
}
else
{
i2c_NACK(i2c_driver);
}
}
soft_i2c_stop(i2c_driver);
return 1;
}
uint8_t soft_i2c_ReadReg_8Bits(soft_i2c_driver *i2c_driver,uint8_t reg)
{
uint8_t data[1];
if(soft_i2c_ReadBytes(i2c_driver,reg,data,1) == 0)
{
return 0xFF;
}
return data[0];
}
uint16_t soft_i2c_ReadReg_16Bits(soft_i2c_driver *i2c_driver,uint8_t reg)
{
uint8_t data[2];
if(soft_i2c_ReadBytes(i2c_driver,reg,data,2) == 0)
{
return 0xFFFF;
}
return data[0]<<8 | data[1];
}
第二点更新如下,添加soft_i2c_waitAckTimeOut,通过滴答定时器提供的时钟判断是否等待超时,注意写函数没有等待超时的判断,如果当前没有反馈ack则直接判断写入失败,等待超时目前只有度函数具有这个功能。
uint8_t soft_i2c_waitAckTimeOut(soft_i2c_driver *i2c_driver)
{
uint32_t startTick;
startTick = HAL_GetTick();
do
{
if(soft_i2c_waitAck(i2c_driver) == 0)
{
return 0;
}
}while(HAL_GetTick() - startTick < SOFT_I2C_TimeOut);
return 1;
}
第三点在soft_i2c_ReadBytes代码中添加了判断是否是读取的最后一个字节,如果不是最后一个字节则返回ack,否则返回Nack
uint8_t soft_i2c_ReadBytes(soft_i2c_driver *i2c_driver,uint8_t reg, uint8_t data[],uint8_t size)
{
uint32_t tick;
soft_i2c_start(i2c_driver);
soft_i2c_Send(i2c_driver,i2c_driver->writeAddr);
if(soft_i2c_waitAckTimeOut(i2c_driver))
{
return 0;
}
soft_i2c_Send(i2c_driver,reg);
if(soft_i2c_waitAckTimeOut(i2c_driver))
{
return 0;
}
soft_i2c_start(i2c_driver);
soft_i2c_Send(i2c_driver,i2c_driver->writeAddr + 1);
if(soft_i2c_waitAckTimeOut(i2c_driver))
{
return 0;
}
for(uint8_t i=0;i<size;i++)
{
data[i] = soft_i2c_Receive(i2c_driver);
if(i+1< size)
{
i2c_ACK(i2c_driver);
}
else
{
i2c_NACK(i2c_driver);
}
}
soft_i2c_stop(i2c_driver);
return 1;
}
代码已经更新在git仓库
如果想要借鉴代码,可以访问
感谢观看
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)