STM32 GPIO模拟i2c通信实现sht20的温湿度采样 并以JSON格式上报(串口调试助手为例)

一、先了解I2C协议

由时钟线SCL和数据线SDA构成的通信线路,利用上拉电阻将它们拉成高电平(表示总线空闲)     

I2C总线可以有多个从设备,且每个从设备都有一个唯一的7bit地址物理识别,因为I2C地址全0为广播地址,所以I2C总线理论上最多能带2^7-1=127个从设备

(I2C:半双工通信的同步串行通信协议,采用电平信号,数据传输采用大端方式MSB,先发高位数据)

I2C总线通信时序:

I2C协议的起始信号(start):当SCL保持高电平时,SDA出现一个下降沿,产生起始位

I2C协议的停止信号(stop):当SCL保持高电平时,SDA出现一个上升沿,产生停止位

(停止通信后,总线空闲,处于高电平)

主设备向从设备发送从设备地址信号,在收到从设备的应答信号后通讯连接才建立成功

若未收到应答则表示寻址失败。

希望继续,则给出“应答(ACK)”信号,即SDA为低电平

不希望继续,则给出“非应答(NACK)”信号,即SDA为高电平

(建立通信后才开始传输数据位)

(读写方向位:RW,0为写操作,1为读操作)

主机发送数据流程:

1、主机在检测到总线为空闲时,发送一个启动信号"S",开始一次通信的开始

2、主机接着发送一个从设备地址,它由7bit物理地址和1bit读写控制位R/W组成(此时RW=0)(发送的地址有7位的物理地址和1位的读写方向号)

3、相对应的从机收到命令字节后向主机回馈应答信号ACK(ACK=D)

4、主机收到从机的应答信号后开始发送第一个字节的数据;

5、从机收到数据后返回一个应答信号ACK;

6、主机收到应答信号后再发送下一个数据字节;

7、主机发完最后一个字节并收到ACK后,向从机发送一个停止信号P结束本次通信并释放总线;

8、从机收到p信号后也退出与主机之间的通信;

主机接收数据流程:

1、主机发送启动信号后,接着发送地址字节(其中R/W=1) :

2、对应的从机收到地址字节后,返回一个应答信号并向主机发送数据;

3、主机收到数据后向从机反馈一个应答信号ACK:

4、从机收到应答信号后再向主机发送下一个数据:

5、当主机完成接收数据后,向从机发送一个NAK,从机收到非应答信号后便停止发送;

6、主机发送非应答信号后,再发送一个停止信号,释放总线结束通信

stm32l431rct6的i2c引脚分配(本例我们使用引脚PB6和PB7为例)

 二、了解sht20

stm32l431rct6的温湿度传感器引脚分配

三、开整

记得把串口使能了(这里我使用的是串口1),如下,其他的我相信你们都配好了(ctr+s就可以直接生成代码哦)

1、在 Core/Src 下创建并编写 SHT20 温湿度传感器的驱动源文件 sht20.c

 

 sht20.c代码如下
#include<stdio.h>
#include "stm32l4xx_hal.h"
#include "sht20.h"
#include "tim.h"

/* 通过该宏控制是使用 HAL 库里的 I2C 接口还是使用 GPIO 模拟串口的接口*/
#define CONFIG_GPIO_I2C

#ifdef CONFIG_GPIO_I2C
#include "gpioi2c.h"
#else
#include "i2c.h"
#endif

/*采样*/
int SHT20_Sample_data(uint8_t cmd, float *data)
{
	uint8_t 	buff[2];
	float 		sht20_data=0.0;
	int			rv;
    /*主设备向发送从设备地址信号,这里sht20的写地址是0x80,成功则返回1个字节*/
	rv=I2C_Master_Transmit(0x80,&cmd,1); 
	if(0!=rv)
	{
		return -1;
	}
	if(cmd==0xf3)
	{
		HAL_Delay(85);
	}
	else if(cmd==0xf5)
	{
		HAL_Delay(29);
	}
    /*主设备向发送从设备地址信号,这里sht20的读地址是0x81,成功则返回2个字节,分别是温湿度的整数                
     *和小数,并且数据放在buff中*/
	rv=I2C_Master_Receive(0x81,buff,2);
	if(0!=rv)
    {
		return -1;
    }
	sht20_data=buff[0];
	sht20_data=ldexp(sht20_data,8);
	sht20_data+=buff[1]&0xFC;
	if(cmd==0xf3)    //0xf3为读温度的信号
	{
		*data=(-46.85+175.72*sht20_data/65536);//计算温度的公式
	}
	else if(cmd==0xf5)    //0xf5为读湿度的信号
	{
		*data=(-6.0+125.0*sht20_data/65536);//计算湿度的公式
	}
	return *data;

}

2、同理,在 Core/Inc 下创建并编写 SHT20 温湿度传感器的驱动头文件 sht20.h

 sht20.h代码如下:
#ifndef INC_SHT20_H_
#define INC_SHT20_H_

#include "stm32l4xx_hal.h"

#define  add_w  0x80  //地址写
#define  add_r  0x81  //地址读
#define measure_temp 0xf3  //读取温度
#define measure_hum 0xf5   //读取湿度

#define user_code_w 0xe6
#define user_code_r 0xe7

#define RST_code 0xfe	//复位

int SHT20_Sample_data(uint8_t cmd, float *data);
#endif /* INC_SHT20_H_ */

3、同理,在 Core/Src 下创建并编写 GPIO 模拟 I2C 驱动源文件 gpioi2c.c

gpioi2c.c代码如下:
#include <stdio.h>
#include "stm32l4xx_hal.h"
#include "tim.h" /* delay_us() implement */
#include "gpio.h"
#include "gpioi2c.h"

#define I2C_CLK_STRETCH_TIMEOUT 50

#define CONFIG_GPIO_I2C_DEBUG
#ifdef CONFIG_GPIO_I2C_DEBUG
#define i2c_print(format,args...) printf(format, ##args)
#else
#define i2c_print(format,args...) do{} while(0)
#endif

/* GPIO Simulate I2C Bus pins */
typedef struct i2c_gpio_s
{
		GPIO_TypeDef *group;
		uint16_t scl; /* SCL */
		uint16_t sda; /* SDA */
} i2c_gpio_t;

static i2c_gpio_t i2c_pins = { GPIOB, GPIO_PIN_6/*SCL*/, GPIO_PIN_7/*SDA*/ };

#define SDA_IN() do{ GPIO_InitTypeDef GPIO_InitStruct = {0}; \
							GPIO_InitStruct.Pin = i2c_pins.sda; \
							GPIO_InitStruct.Mode = GPIO_MODE_INPUT; \
							GPIO_InitStruct.Pull = GPIO_PULLUP; \
							GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; \
							HAL_GPIO_Init(i2c_pins.group, &GPIO_InitStruct); \
						}while(0)

#define SDA_OUT() do{ GPIO_InitTypeDef GPIO_InitStruct = {0}; \
							GPIO_InitStruct.Pin = i2c_pins.sda; \
							GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; \
							GPIO_InitStruct.Pull = GPIO_PULLUP; \
							GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; \
							HAL_GPIO_Init(i2c_pins.group, &GPIO_InitStruct); \
						}while(0)

#define SCL_OUT() do{ GPIO_InitTypeDef GPIO_InitStruct = {0}; \
							GPIO_InitStruct.Pin = i2c_pins.scl; \
							GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; \
							GPIO_InitStruct.Pull = GPIO_PULLUP; \
							GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; \
							HAL_GPIO_Init(i2c_pins.group, &GPIO_InitStruct); \
						}while(0)

#define SCL_H() HAL_GPIO_WritePin(i2c_pins.group, i2c_pins.scl, GPIO_PIN_SET)
#define SCL_L() HAL_GPIO_WritePin(i2c_pins.group, i2c_pins.scl, GPIO_PIN_RESET)
#define SDA_H() HAL_GPIO_WritePin(i2c_pins.group, i2c_pins.sda, GPIO_PIN_SET)
#define SDA_L() HAL_GPIO_WritePin(i2c_pins.group, i2c_pins.sda, GPIO_PIN_RESET)
#define READ_SDA() HAL_GPIO_ReadPin(i2c_pins.group, i2c_pins.sda)
#define READ_SCL() HAL_GPIO_ReadPin(i2c_pins.group, i2c_pins.scl)

static inline uint8_t I2c_WaitWhileClockStretching(uint16_t timeout)
{
		while( timeout-- > 0 )
		{
			if( READ_SCL() )
				break;
			delay_us(1);
        }
		return timeout ? NO_ERROR : BUS_ERROR;
}

/* StartCondition(S) */
uint8_t I2c_StartCondition()
{
			uint8_t rv = NO_ERROR;
			SDA_OUT();
			SCL_OUT();
/* StartCondition(S): A high to low transition on the SDA line while SCL is high.
		_______
SCL: 		   |___
		_____
SDA: 		 |_____
*/
			SDA_H();
				delay_us(1);
			SCL_H();
				delay_us(1);
#ifdef I2C_CLK_STRETCH_TIMEOUT
			rv = I2c_WaitWhileClockStretching(I2C_CLK_STRETCH_TIMEOUT);
			if( rv )
			{
				i2c_print("ERROR: %s() I2C bus busy\n", __func__);
				return rv;
            }
#endif
			SDA_L();
			delay_us(2);
			SCL_L();
			delay_us(2);
			return rv;
}

/* StopCondition(P) */
uint8_t I2c_StopCondition(void)
{
			uint8_t rv = NO_ERROR;
			SDA_OUT();
/* StopCondition(P): A low to high transition on the SDA line while SCL is high.
		 _____
SCL: ___|
			_____
SDA: ______|
*/
			SCL_L();
			SDA_L();
			delay_us(2);
			SCL_H();
			delay_us(2);
#ifdef I2C_CLK_STRETCH_TIMEOUT
			rv = I2c_WaitWhileClockStretching(I2C_CLK_STRETCH_TIMEOUT);
			if( rv )
			{
				i2c_print("ERROR: %s() I2C bus busy\n", __func__);
			}
#endif
			SDA_H();
			delay_us(2);
			return rv;
}
uint8_t I2c_WriteByte(uint8_t byte)
{
			uint8_t rv = NO_ERROR;
			uint8_t mask;
			/* Data line changes must happened when SCL is low */
			SDA_OUT();
			SCL_L();
			/* 1Byte=8bit, MSB send: bit[7]-->bit[0] */
			for(mask=0x80; mask>0; mask>>=1)
			{
				if((mask & byte) == 0)
				{
					SDA_L();
				}
				else
				{
					SDA_H();
				}
				delay_us(5); // data set-up time (t_SU;DAT)
				SCL_H();
				delay_us(5); // SCL high time (t_HIGH)
#ifdef I2C_CLK_STRETCH_TIMEOUT
				rv = I2c_WaitWhileClockStretching(I2C_CLK_STRETCH_TIMEOUT);
				if( rv )
				{
					i2c_print("ERROR: %s() I2C bus busy\n", __func__);
					goto OUT;
				}
#endif
				SCL_L();
				delay_us(5); // data hold time(t_HD;DAT)
			}
			/* clk #9 wait ACK/NAK from slave */
			SDA_IN();
			SCL_H(); // clk #9 for ack
			delay_us(5); // data set-up time (t_SU;DAT)
#ifdef I2C_CLK_STRETCH_TIMEOUT
			rv = I2c_WaitWhileClockStretching(I2C_CLK_STRETCH_TIMEOUT);
			if( rv )
			{
				i2c_print("ERROR: %s() I2C bus busy\n", __func__);
				goto OUT;
			}
#endif
			 /* High level means NAK */
			if( READ_SDA() )
			rv = ACK_ERROR;
OUT:
			SCL_L();
			delay_us(20);
			return rv;
}

uint8_t I2c_ReadByte(uint8_t *byte, uint8_t ack)
{
		uint8_t rv = NO_ERROR;
		uint8_t mask;
		*byte = 0x00;
		SDA_IN();
		/* 1Byte=8bit, MSB send: bit[7]-->bit[0] */
		for(mask = 0x80; mask > 0; mask >>= 1)
		{
			SCL_H(); // start clock on SCL-line
			delay_us(1); // clock set-up time (t_SU;CLK)
#ifdef I2C_CLK_STRETCH_TIMEOUT
			rv = I2c_WaitWhileClockStretching(I2C_CLK_STRETCH_TIMEOUT);
			if( rv )
			{
				i2c_print("ERROR: %s() I2C bus busy\n", __func__);
				goto OUT;
			}
#endif
			if(READ_SDA())
				*byte |= mask; // read bit
			SCL_L();
			delay_us(2); // data hold time(t_HD;DAT)
		}
		/* clk #9 send ACK/NAK to slave */
		if(ack == ACK)
		{
			SDA_OUT();
			SDA_L(); // send Acknowledge if necessary
		}
		else if( ack == NAK )
		{
			SDA_OUT();
			SDA_H(); // send NotAcknowledge if necessary
		}
		delay_us(1); // data set-up time (t_SU;DAT)
		SCL_H(); // clk #9 for ack
		delay_us(2); // SCL high time (t_HIGH)
#ifdef I2C_CLK_STRETCH_TIMEOUT
		rv = I2c_WaitWhileClockStretching(I2C_CLK_STRETCH_TIMEOUT);
		if( rv )
		{
		 i2c_print("ERROR: %s() I2C bus busy\n", __func__);
		}
#endif
OUT:
		SCL_L();
		delay_us(2); // wait to see byte package on scope
		return rv;
}

uint8_t I2c_SendAddress(uint8_t addr)
{
     return I2c_WriteByte(addr);
}

int I2C_Master_Receive(uint8_t addr, uint8_t *buf, int len)
{
			int i;
			int rv = NO_ERROR;
			uint8_t byte;
			I2c_StartCondition();
			rv = I2c_SendAddress(addr);
			if( rv )
			{
				i2c_print("Send I2C read address failure, rv=%d\n", rv);
				goto OUT;
			}
#ifdef I2C_CLK_STRETCH_TIMEOUT
			/* wait while clock streching */
			rv = I2c_WaitWhileClockStretching(I2C_CLK_STRETCH_TIMEOUT);
			if( rv )
			{
				i2c_print("ERROR: %s() I2C wait clock stretching failure, rv=%d\n", __func__, rv);
				return rv;
			}
#endif
			for (i=0; i<len; i++)
			{
				if( !I2c_ReadByte(&byte, ACK) )
				{
					buf[i] = byte;
				}
				else
					goto OUT;
			}
OUT:
			I2c_StopCondition();
			return rv;
}

int I2C_Master_Transmit(uint8_t addr, uint8_t *data, int bytes)
{
			int i;
			int rv = NO_ERROR;
			if(!data)
			{
			 return PARM_ERROR;
			}
			 i2c_print("I2C Mastr start transimit [%d] bytes data to addr [0x%02x]\n", bytes, addr);
			 I2c_StartCondition();
			  rv = I2c_SendAddress(addr);
			if( rv )
			{
				goto OUT;
			}
	for (i=0; i<bytes; i++)
	{
		  if( NO_ERROR != (rv=I2c_WriteByte(data[i])) )
		{
		  break;
		}
	}
OUT:
  I2c_StopCondition();
  return rv;
}

4、同理,在 Core/Inc 下创建并编写 GPIO 模拟 I2C 头文件 gpioi2c.h

gpioi2c.h代码如下:

enum
{
	NO_ERROR = 0x00, // no error
	PARM_ERROR = 0x01, // parameter out of range error
	ACK_ERROR = 0x02, // no acknowledgment error
	CHECKSUM_ERROR = 0x04, // checksum mismatch error
	TIMEOUT_ERROR = 0x08, // timeout error
	BUS_ERROR = 0x10, // bus busy
};
enum
{
	ACK_NONE, /* Without ACK/NAK Reply */
	ACK, /* Reply with ACK */
	NAK, /* Reply with NAK */
};
extern int I2C_Master_Receive(uint8_t addr, uint8_t *buf, int len);
extern int I2C_Master_Transmit(uint8_t addr, uint8_t *data, int bytes);

5、修改main.c中的代码,找到下列相应位置,添加如图代码即可

 

 编写好之后,点击运行代码

 将程序烧录至开发板

 

串口调试助手连接板子,在板子上按复位键就可以看到打印信息了,如下: