基于Stm32f103利用模拟iic驱动LM75A温度传感器

这两天一直在搞模拟iic,模拟iic相较于硬件iic的优势在于更稳定,io口的选择更灵活。

这次编写模拟iic驱动程序还是有点坎坷,其中模拟iic的时序不是难点,直接说我遇到的问题1.io口模式的选择(一开始我是使用固定的io口模式,这中间也看到有人说io口模式不影响,经过我的实践是会影响的)2.LM75A驱动时序图的理解不到位。

下面分析遇到这两个拦路虎的原因:第一个问题是对硬件电路的一些知识不掌握,没有搞清io口模式具体的含义,第二个问题我认为还是对iic协议的不熟悉。

下面具体说说模拟iic驱动编写的过程(这里和我写的驱动TM1640的地方有异曲同工之处)

首先我们还是要看一下iic通讯协议的时序图

这张图就很清楚明白了,我们可以把上图分解成几个部分:启动信号部分、8位数据发送(数据接受与发送其本质是一样的)部分、应答(这里应答又可以分为主机发送应答信号,主机接受从机应答信号)部分、停止信号部分。

这个是LM75A中读取temp寄存器的步骤,这里解释一下器件应答和主机应答,器件应答为从机向主机发送应答信号0(正常工作)、1(非正常工作),而主机应答为主机向从机发送应答信号0(应答)、1(非应答),为什么要解释这一点呢?这是与后面io口SDA脚的初始化有关,因为器件应答需要读取SDA引脚的电平,故要将io口设置为浮空输入,而主机应答需要向从机发送信号,故需要设置成推挽输出(这里建议明白各种io口模式的作用,你可以搞完后设置成其他模式看看能不能正常工作)

下面是.h文件,有一些宏定义,在.c文件中编写方便一些

#ifndef __SOFT_IIC_H
#define __SOFT_IIC_H
#include "headfile.h"

#define SCL GPIO_Pin_6 //GPIOB
#define SDA GPIO_Pin_7
#define SCL_High GPIO_WriteBit(GPIOB,SCL,Bit_SET);
#define SDA_High GPIO_WriteBit(GPIOB,SDA,Bit_SET);
#define SCL_Low GPIO_WriteBit(GPIOB,SCL,Bit_RESET);
#define SDA_Low GPIO_WriteBit(GPIOB,SDA,Bit_RESET);

void Soft_IIC_Mode(uint8_t mode);
void Soft_IIC_Start(void);
void Soft_IIC_End(void);
void Soft_IIC_Ack(void);
void Soft_IIC_NoAck(void);
void Soft_IIC_WaitAck(void);
void Soft_IIC_BasicSend(uint8_t data);
uint8_t Soft_IIC_BasicRead(void);
uint8_t Soft_IIC_ReadByte(uint8_t SlaveAddr,uint8_t ReadAddr);
void Soft_IIC_ReadBuffer(uint8_t SlaveAddr,uint8_t ReadAddr,uint8_t* ReadBuffer,uint8_t Num);


#endif

由于我们上面说到不同部分用到的SDA脚io口模式不一样,所以我们编写一个引脚模式选择函数

void Soft_IIC_Mode(uint8_t mode)
{
    switch(mode)
    {
        case 0: RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//输出模式
        
                GPIO_InitTypeDef GPIOB_InitStrcture;
                GPIOB_InitStrcture.GPIO_Mode=GPIO_Mode_Out_PP;
                GPIOB_InitStrcture.GPIO_Pin= SDA | SCL;
                GPIOB_InitStrcture.GPIO_Speed=GPIO_Speed_50MHz;
    
                GPIO_Init(GPIOB,&GPIOB_InitStrcture);break;
        
        case 1: RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
        
                GPIOB_InitStrcture.GPIO_Mode=GPIO_Mode_IN_FLOATING;//输入模式
                GPIOB_InitStrcture.GPIO_Pin=SDA;
                GPIOB_InitStrcture.GPIO_Speed=GPIO_Speed_50MHz;
    
                GPIO_Init(GPIOB,&GPIOB_InitStrcture);break;
    }
}

下面我们结合iic时序图编写程序

启动程序代码

void Soft_IIC_Start(void)
{
    Soft_IIC_Mode(0);
    SDA_High;
    SCL_High;
    Delay_us(5);//确保信号正确
    SDA_Low;
    Delay_us(5);//先拉低SDA
    SCL_Low;
}

对照上面启动的时序图,为了确保信号的正确,我们先将SDA,SCL都拉高,延时5us是为了信号的稳定(5us应该可以更短,可以查询数据手册修改,提高通讯效率),然后拉低SDA,到这里其实已经发出了启动信号,但是我为了后续发送数据部分,SCL操作对称且好理解,在启动信号后将SCL拉低,这样在后续发送一字时只需改变SDA电平(数据,在SCL为低店电平时才能改变),再将SCL拉高(发送出去)

所以在后面任何部分的代码,你都会发现最后一步操作都是将SCL拉低,这样可以使每部分衔接更加清晰,不会出现SCL混乱的情况。

然后就是发送数据部分(8位)代码

void Soft_IIC_BasicSend(uint8_t data)
{
    Soft_IIC_Mode(0);
    for(uint8_t i=0;i<8;i++)
    {
        if(data & 0x80)
        {
           SDA_High;
           Delay_us(5);
        }
        else
        {
           SDA_Low;
           Delay_us(5);
        }
        data<<=1;
        SCL_High;
        Delay_us(5);
        SCL_Low;
        Delay_us(5);
    }
}

这里就把刚刚启动信号完成后将SCL拉低的优势体现出来了,便于理解这部分代码。启动信号之后SCL保持在低电平,进for循环直接可以改变SDA,注意的是:iic中数据的传输都是先传输的高位,故这里用了向左位移,一位一位的从高位将数据传输出去。

一开始说到,发送与读取本质是一样,发送是将SCL拉低后,改变SDA,再拉高SCL发送出去;而读取则是拉低SCL再拉高,当SCL为高电平时,读取SDA的电平(即为从机发送的数据)

代码如下

特别注意的是,要读取器件SDA时,都需要将SDA拉高释放总线,这样才能准确读取从机SDA信号。

并且整个iic中只要涉及读取器件SDA的都需要将SDA的io口模式设置成输入模式!!!

uint8_t Soft_IIC_BasicRead(void)
{
    uint8_t BasicRead=0;
    
    Soft_IIC_Mode(1);
    SDA_High;//释放SDA总线
    for(uint8_t i=0;i<8;i++)
    {
        SCL_High;
        Delay_us(5);
        BasicRead<<=1;
        if(GPIO_ReadInputDataBit(GPIOB,SDA))
        {
            BasicRead++;
            Delay_us(1);
        }
        SCL_Low;
        Delay_us(5);
    }

    return BasicRead;
}

接下来就是应答代码

首先是主机应答与不应答(这里是由主机产生的,故SDA仍为输出模式)

主机应答相当于在8位数据发送后,再发送第九位应答位。

void Soft_IIC_Ack(uint8_t Ack)
{
    Soft_IIC_Mode(0);
    switch(Ack)
    {
        case 0:SDA_Low;Delay_us(5);break;//0为发送应答信号
        case 1:SDA_High;Delay_us(5);break;//1为发送非应答信号
    }
    SCL_High;
    Delay_us(5);
    SCL_Low;
}

其次是器件应答(这里是由从机产生的信号,故SDA需要设置为输入模式)

void Soft_IIC_WaitAck(void)
{
    uint8_t timer=0;
    
    Soft_IIC_Mode(1);
    SDA_High;
    SCL_High;//释放总线,由从机发SDA信号
    while( GPIO_ReadInputDataBit(GPIOB,SDA) )
    {
        if(timer++>100)
        {
            Soft_IIC_End();
        }
        
    }
    SCL_Low;
}

其中如果从机应答一直发回的是1(即不正常工作,则结束本次通讯)。

最后就是结束通讯代码(与启动信号类似)

void Soft_IIC_End(void)
{
    Soft_IIC_Mode(0);
    SDA_Low;
    SCL_Low;
    Delay_us(5);//确保信号正确
    SCL_High;
    Delay_us(5);
    SDA_High;
}

以上就是全部模拟iic部分的代码了,后续驱动LM75A温度传感器只需要调用他们即可,在上一篇中讲了利用硬件iic驱动(基于标准库函数)

接下来讲一下利用模拟iic驱动以及遇到的问题

我们还是只讲一下温度寄存器的读取(不讲读取值处理成实际温度,也很简单,有需要可以参考上一篇)

只需要看图10读取temp寄存器的2字节数据,结合下面解释耐心看一下,对整个步骤有所了解

void Soft_IIC_ReadBuffer(uint8_t SlaveAddr,uint8_t ReadAddr,uint8_t* ReadBuffer,uint8_t Num)
{
    Soft_IIC_Start();
    Soft_IIC_BasicSend(SlaveAddr);
    Soft_IIC_WaitAck();
    Soft_IIC_BasicSend(ReadAddr);
    Soft_IIC_WaitAck();
    Soft_IIC_Start();
    Soft_IIC_BasicSend( (SlaveAddr+1) );
    Soft_IIC_WaitAck();
    while(Num)
    {
        if(Num == 1)//只剩下最后一个数据时进入 if 语句
        { 
            *ReadBuffer = Soft_IIC_BasicRead();//调用库函数将数据取出到 Buffer
            Num--; //字节数减 1 
            Soft_IIC_Ack(1); //最后有一个数据时关闭应答位
            Soft_IIC_End();    //最后一个数据时使能停止位
        }
        else//读取数据
        { 
             *ReadBuffer = Soft_IIC_BasicRead();//调用库函数将数据取出到 Buffer
             ReadBuffer++; //指针移位
             Num--; //字节数减 1 
             Soft_IIC_Ack(0);
        }
    }
}

上面就是将图10翻译过来的代码,你可能会有一些疑问,从图10可以看出我们的器件地址实际只有7位,但是我们需要发送8位(因为你会发现7位地址后面还有一位决定读写位(0为写,1为读)这里说明其中器件地址SlaveAddr我们默认第八位读写为0(写),所以在下面我们需要读的时候只需要+1第八位读写位就变成了1(这里就是卡了我一天的坑),因为在硬件iic中调用库函数,没有考虑这一点。

这样就完完整整把这个时序图翻译过来了,从而就可以顺利驱动LM75A,亲眼看到数码管上读取的错误值到正确的温度值,这是很欣慰的,其中还经历了很多查错才发现这个问题。

有些东西必须知道它为什么存在(存在即合理),有些人说不必要重复造车轮,我同意这个观点,但是我还是认为知道车轮是怎么造的更好。可能这次写了模拟iic的代码后,之后都是Ctrl+C了,用起来心里也有底些。。

其次很多关于iic通讯的专业术语我可能表达的并不准确,如有错误,敬请指正,在错误中进步嘛!

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
STM32F103可以通过软件模拟I2C总线,下面是一个简单的例子,可以供参考: ```c #include "stm32f10x.h" #define SDA_H GPIO_SetBits(GPIOB,GPIO_Pin_7) //SDA输出高电平 #define SDA_L GPIO_ResetBits(GPIOB,GPIO_Pin_7) //SDA输出低电平 #define SCL_H GPIO_SetBits(GPIOB,GPIO_Pin_6) //SCL输出高电平 #define SCL_L GPIO_ResetBits(GPIOB,GPIO_Pin_6) //SCL输出低电平 #define SDA_read GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_7) //读取SDA口状态 void IIC_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //使能GPIOB时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; //选择GPIOB的6、7口 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //最大输出频率50MHz GPIO_Init(GPIOB, &GPIO_InitStructure); //初始化GPIOB的6、7口 SDA_H; SCL_H; } void IIC_Start(void) { SDA_H; SCL_H; delay_us(4); SDA_L; delay_us(4); SCL_L; } void IIC_Stop(void) { SDA_L; SCL_H; delay_us(4); SDA_H; delay_us(4); } void IIC_Ack(void) { SDA_L; delay_us(4); SCL_H; delay_us(4); SCL_L; delay_us(4); SDA_H; delay_us(4); } void IIC_NAck(void) { SDA_H; delay_us(4); SCL_H; delay_us(4); SCL_L; delay_us(4); } uint8_t IIC_WaitAck(void) { uint8_t ucErrTime = 0; SDA_H; delay_us(1); SCL_H; delay_us(1); while(SDA_read) { ucErrTime++; if(ucErrTime > 250) { IIC_Stop(); return 1; } } SCL_L; return 0; } void IIC_SendByte(uint8_t txd) { uint8_t t; SDA_L; for(t=0;t<8;t++) { if((txd&0x80) >> 7) SDA_H; else SDA_L; txd <<= 1; SCL_H; delay_us(4); SCL_L; delay_us(4); } } uint8_t IIC_ReadByte(unsigned char ack) { unsigned char i,receive=0; SDA_H; for(i=0;i<8;i++) { SCL_H; delay_us(4); receive <<= 1; if(SDA_read) receive++; SCL_L; delay_us(4); } if (ack) IIC_Ack(); else IIC_NAck(); return receive; } void IIC_WriteByte(uint8_t device_addr, uint8_t reg_addr, uint8_t data) { IIC_Start(); IIC_SendByte(device_addr<<1); IIC_WaitAck(); IIC_SendByte(reg_addr); IIC_WaitAck(); IIC_SendByte(data); IIC_WaitAck(); IIC_Stop(); } uint8_t IIC_ReadByte(uint8_t device_addr, uint8_t reg_addr) { uint8_t data; IIC_Start(); IIC_SendByte(device_addr<<1); IIC_WaitAck(); IIC_SendByte(reg_addr); IIC_WaitAck(); IIC_Start(); IIC_SendByte((device_addr<<1) + 1); IIC_WaitAck(); data = IIC_ReadByte(0); IIC_Stop(); return data; } ``` 这里用到了GPIOB的6、7口作为I2C总线的SCL和SDA,通过不同的函数可以实现I2C总线的初始化,发送数据和读取数据等操作。需要注意的是,软件模拟I2C总线的速度相对硬件I2C总线较慢,因此在实际使用时需要根据具体情况进行优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值