6-硬件I2C读取DHT12数据
试用了一段时间之后,发现CH582的库函数用起来简单、方便,很容易入门。但是部分例程有些欠缺,而且文档的表述有点宽泛,不够详细。比如I2C控制寄存器中POS位如何控制NACK的发送?在这篇评测的最后对疑问进行了描述,希望专业人士解答一下。
这次评测使用硬件I2C,对DHT12的温湿度数据进行读取。
I2C协议简要说明
I2C只使用2根数据线进行通信:SDA和SCL,即数据线和通信线。为了实现主从机的双向通信,SCL和SDA引脚需要使用开漏的方式,因此需要上拉电阻(内部上拉或者外部上拉)。
每一组数据都是由START位开始,并由STOP位停止。START位和STOP位都是由主机发起的。START位后跟1个或2个字节的地址位,当从机判断到自己的地址后需要回复1位的ACK,之后则是数据字节,同样每个字节之后,接收方都需要以ACK回应。数据传输完成后主机发送STOP位。
对应到MCU的内部,当检测到START位、地址或者接收/发送完1字节数据时,则是通过状态寄存器中的标志位表示状态。比如几个比较重要的标志位:
- TxE、RxNE分别表示在发送和接收状态时数据寄存器的状态
- SB、STOPF表示是否发送了START位和是否检测到STOP位
- BTF字节发送结束标志位
硬件I2C的操作
CH582读取DHT12时,CH582作为主设备进行通信,需要完成数据的写和读操作。
初始化代码如下,注意默认的I2C引脚与开发板上的TYPE-A USB的引脚共用,因此不能使用这个USB插口。
void i2c_config()
{
GPIOB_ModeCfg( GPIO_Pin_13|GPIO_Pin_12, GPIO_ModeIN_PU );
I2C_Init( I2C_Mode_I2C, 10000, I2C_DutyCycle_16_9, I2C_Ack_Enable, I2C_AckAddr_7bit, TxAdderss );
I2C_StretchClockCmd(ENABLE);
I2C_Cmd(ENABLE);
}
主设备写操作,在I2C的例程中有直接的例子可以参考,可以根据例子做些修改。从下面的代码中可以看出,在发送START位之前首先要判断总线是否正在使用。然后发送START位和从机地址,等到从机应答了ACK之后状态寄存器的ADDR位将会置位,这时就可以开始数据的发送了。
void i2c_transfer_data(uint8_t addr,uint8_t data_len,uint8_t* data)
{
uint8_t i=0;
uint32_t flag1 = 0, flag2 = 0;
while( I2C_GetFlagStatus( I2C_FLAG_BUSY ) != RESET );
I2C_GenerateSTART( ENABLE );
while( !I2C_CheckEvent( I2C_EVENT_MASTER_MODE_SELECT ) );
I2C_Send7bitAddress( addr, I2C_Direction_Transmitter );
while( !I2C_CheckEvent( I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED ) );
while( i<data_len )
{
if( I2C_GetFlagStatus( I2C_FLAG_TXE ) != RESET )
{
I2C_SendData( data[i] );
i++;
}
}
while( !I2C_CheckEvent( I2C_EVENT_MASTER_BYTE_TRANSMITTED ) );
I2C_GenerateSTOP( ENABLE );
}
主设备的读操作,这个在例程中没有,在读操作里有个特别的地方是接收最后一个字符的时候主设备需要回复NACK并且发送STOP位。通过了几次实验发现I2C_CTRL1控制寄存器里的POS位和ACK位并不能控制NACK的发送,主要起作用的还是STOP位。
发送STOP位时要注意,当只接收1个字节的时候,需要在清楚ADDR标志位后紧接着置位STOP位。在接收其它长度数据时,需要在接收的倒数第2个字节时置位STOP位,代码如下。这是因为在软件判断到RxNE置位,并且读取数据寄存器的时候,总线已经在接收下一个字节了,这下一个字节已经位于以为寄存器里了。
void i2c_recv_data(uint8_t addr,uint8_t data_len,uint8_t* data)
{
uint8_t i=0;
while( I2C_GetFlagStatus( I2C_FLAG_BUSY ) != RESET );
I2C_GenerateSTART( ENABLE );
while( !I2C_CheckEvent( I2C_EVENT_MASTER_MODE_SELECT ) );
I2C_Send7bitAddress( addr, I2C_Direction_Receiver );
while( !I2C_CheckEvent( I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED ) );
if(data_len == 1){
I2C_GenerateSTOP( ENABLE );
if( I2C_GetFlagStatus( I2C_FLAG_RXNE ) != RESET )
{
data[i] = I2C_ReceiveData( );
}
}
else{
while( i<(data_len) )
{
if( I2C_GetFlagStatus( I2C_FLAG_RXNE ) != RESET )
{
data[i] = I2C_ReceiveData( );
i++;
if(i == (data_len-1) ){
I2C_GenerateSTOP( ENABLE );
}
}
}
}
}
一些小疑惑的解答
在使用I2C的时候遇到了一些问题,通过实验看波形,得到了一些解答,在下面列出来。
I2C有时钟延长的功能,这个功能能够缓解数据接收方的工作负荷,那么什么情况下会出现时钟延长的状态呢?
- 接收方:当接收到数据,数据在移位寄存器里将要存入数据寄存器时,如果此时上一个数据还在数据寄存器中未被取出,这时接收方将会把SCL拉低,等到上一个数据从数据寄存器中取出,并且当前数据从移位寄存器存入到数据寄存器后,才释放SCL。
- 发送方:当数据寄存器空并且移位寄存器的数据也发送完之后,会拉低SCL,等待数据寄存器中放入新的数据。
ACK回复是发生在时钟延长之前还是之后?
ACK发生在时钟延长之前。
遇到的问题
问题:I2C_CTRL1的POS位和ACK位怎么配合能够实现在第二个字符时发送NACK呢?我的代码如下,清除了ADDR位后,判断如果只接受2个字节,那么就清除ACK位,置位POS位。但是从波形上看,接收到第2个字节的时候回复的还是ACK,并且通信也没有自动进入从模式。貌似POS位并没有什么作用,要想回复NACK,还是要靠STOP位。希望专业人士解答一下。
```c
//在清除ADDR位后
else if(data_len == 2){
I2C_AcknowledgeConfig(DISABLE);
I2C_NACKPositionConfig(ENABLE);
while( i<(data_len) )
{
if( I2C_GetFlagStatus( I2C_FLAG_RXNE ) != RESET )
{
data[i] = I2C_ReceiveData( );
i++;
}
}
I2C_GenerateSTOP( ENABLE );
}
```