解决 I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT) 卡住问题和一个STM32F103的IIC示例

很多人都说STM32F103自带的硬件IIC不好用,其中,问的问题最多的是在 I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT) 这个地方卡住。其实吧,ST这么大的公司,也不至于在IIC这种基础总线上犯这么大的错误。

我使用的实际情况是IIC总线接口是单机热拔插的,我发现如果是冷插入,就是先连接好设备,在上电,就没什么问题,或者是先给从机上电,然后主机重启之后就一定没问题了。这很奇怪,我感觉是遇到了Error,于是就打开了所有的Error中断,最后发现在接口被拔下的时候,或者重新插上,会进入Bus Error  Flag,处理器会认为总线出了问题,当然没办法检测到已经设置为主机。我尝试了清除这些错误标记,但是依然不能用。

这个问题的一个解决办法是:每次主机发送完毕就关闭IIC功能,然后下次发送的时候重新初始化。

首先贴一个初始化的代码(STM32F103XXXX,固件库:V3.5.0):

void IIC_Configuration()
{
    // Clock Enable
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO | RCC_APB2Periph_GPIOB, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
    // Port Setting
    GPIO_InitTypeDef  GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    // IIC Setting
    I2C_InitTypeDef I2C_InitStruct;
    I2C_InitStruct.I2C_ClockSpeed = 400000;
    I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;
    I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;
    I2C_InitStruct.I2C_OwnAddress1 = 0x55;
    I2C_InitStruct.I2C_Ack = I2C_Ack_Enable;
    I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
    I2C_Init(I2C1, &I2C_InitStruct);
    // IIC Enable
    I2C_Cmd(I2C1, ENABLE);
}

发送时,首先要检测总监是否空闲,然后发送一个起始位,然后检查是否已经设置为主机模式,主要是检测总线是否有错误之类的,可以进去仔细跟每一位都有什么,我卡住的时候,跟到的是总线错误以及有未知的未读取的数据,因为我连接的两个从机在上电的时候其中一个也作为主机进行通信。然后发送地址,并检测状态。通过之后就可以发送数据了,然后检测是否发送完毕,最后发送一个结束标记。整个通信就完成了。具体代码如下:

while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));// Wait IIC Bus Idle
I2C_GenerateSTART(I2C1, ENABLE);// Send Start Flag
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));// Wait Master Mode Ready
I2C_Send7bitAddress(I2C1, addr, I2C_Direction_Transmitter);// Send Slave Address
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));// Wait TX Mode Ready
I2C_SendData(I2C1, 0xAA);// Send One Byte
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));// Wait Transmittion
I2C_GenerateSTOP(I2C1, ENABLE);// Send Stop Flag

这段代码可以基本解决发送,大家基本上卡在第三行或者第五行。所以在这段代码的最前面加上:

IIC_Configuration();

作用就是在发送之前再初始化,相应的,在MCU初始化的时候取消IIC的初始化。并且,在最后面加上:

I2C_Cmd(I2C1, DISABLE);

我是这样解决的我遇到的卡住问题的。虽然感觉也不是最根本的解决了问题,因为飞利浦公司的官方文档里写了IIC是支持热拔插的。不过经过很多次测试,这个方法很稳定。

最后说一下卡死这个问题,卡死是一个特别低级和特别要命的问题。一个好的单片机嵌入式程序(应该说是所有程序),遇到任何错误,都不应该卡死。卡死给所有用户的交互体验都是最糟糕的。像很多人习惯使用while循环来判定一个状态标记位,这其实是非常偷懒和不负责任的用法,最多在开发初期使用,之后一定要换成安全可靠的方案。下面提供我使用的一个方式:

首先,在开发初期,为了快速验证,难免会使用while来判定状态,这时候,就直接避免掉,以上文中的 while(!I2C_CheckEvent(I2C1, I2C_EVENT_XXXXXXXXX)); 为例:使用一个u8型的函数,Safe_I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG) 来代替。这个函数的写法如下:

uint8_t Safe_I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG) 
{
    while(!I2C_CheckEvent(I2Cx, I2C_FLAG))
    {
        //Complete Later #1
    }
    return 0; //Completed Flag
}

然后把原来程序里的while写法全部替换掉,虽然这时候的执行效果是一致的。在解决完基础问题之后,重新写 #1 部分。在内部加入超时判定,同时也可以做一些需要高速做的事情。如果判定为超时,则 return 1。这时候,我们会发现一个小问题,就是程序里并没有支持对于return 1之后的退出处理,反而return 1之后程序会继续运行,造成故障。

这时候,我推荐使用#define大法。#define是一个被诟病很多,但其实用好了非常神奇的预处理语句。我们把Safe_I2C_CheckEvent这个函数的名字改成 Safe_I2C_CheckEvent_Function,反正名字再长也无所谓嘛。然后写下如下代码:

#define Safe_I2C_CheckEvent(x,y) if (Safe_I2C_CheckEvent_Function(x,y)) return 1

这句话的意思就等于是保留了Safe_I2C_CheckEvent这个名字,这样原来程序里的内容都不需要变。并且,带有了可以让发送函数也return 1返回发送失败标记的功能。是不是非常巧妙!

今天就写到这里,希望大家能够解决问题!