小熊派GD32开发(7)— 使用软件模拟I2C读取SHT30温湿度传感器
一、编写软件模拟I2C驱动程序
在【Devices】下新建soft_i2c.c
和soft_i2c.h
文件,首先,查看硬件原理图,可以看到,IIC_SCL使用PB6引脚,IIC_SDA使用PB7引脚,将这两个引脚初始化即可:
/* 软件模拟IIC引脚初始化
* IIC_SCL --> PB6
* IIC_SDA --> PB7 */
void IIC_Init(void)
{
rcu_periph_clock_enable(RCU_GPIOB); /* 使能GPIOB时钟 */
/* 配置IIC_SCL引脚为推挽输出 */
gpio_init(GPIOB, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_6);
/* 配置IIC_SDA引脚为推挽输出 */
gpio_init(GPIOB, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_7);
gpio_bit_set(GPIOB, GPIO_PIN_6);
gpio_bit_set(GPIOB, GPIO_PIN_7);
}
因为模拟IIC通信里需要用到us延时,所以,编写一个us延时函数
/* 描述:us级延时函数
* 参数nus:需要延时的us数
* 返回值:无*/
void delay_us(uint32_t nus)
{
uint32_t ticks;
uint32_t told, tnow, tcnt = 0;
uint32_t reload = SysTick->LOAD; /* 滴答定时器的重装载值 */
ticks = nus * 120; /* 需要的节拍数 */
told = SysTick->VAL; /* 刚进入时的计数器值 */
while(1)
{
tnow = SysTick->VAL;
if(tnow != told)
{
if(tnow < told)tcnt += told - tnow;
else tcnt += reload - tnow + told;
if(tcnt >= ticks)break; /* 时间超过/等于要延迟的时间,则退出. */
told = tnow;
}
}
}
模拟IIC通信的函数如下
/* 描述:启动I2C总线,即发送I2C起始条件.
* 参数: 无
* 返回值:无 */
void IIC_Start(void)
{
SDA_OUT();
IIC_SDA(1);
IIC_SCL(1);
delay_us(4);
IIC_SDA(0);
delay_us(4);
IIC_SCL(0);
}
/* 描述:结束I2C总线,即发送I2C结束条件.
* 参数: 无
* 返回值:无 */
void IIC_Stop(void)
{
SDA_OUT();
IIC_SCL(0);
IIC_SDA(0);
delay_us(4);
IIC_SCL(1);
delay_us(4);
IIC_SDA(1);
delay_us(4);
}
/* 描述:发送应答 ACK
* 参数: 无
* 返回值:无 */
void IIC_ACK(void)
{
SDA_OUT();
IIC_SCL(0);
delay_us(2);
IIC_SDA(0);
delay_us(2);
IIC_SCL(1);
delay_us(2);
IIC_SCL(0);
delay_us(1);
}
/* 描述:发送非应答 NACK
* 参数: 无
* 返回值:无 */
void IIC_NACK(void)
{
SDA_OUT();
IIC_SCL(0);
delay_us(2);
IIC_SDA(1);
delay_us(2);
IIC_SCL(1);
delay_us(2);
IIC_SCL(0);
delay_us(1);
}
/* 描述:等待ACK
* 参数: 无
* 返回值:等待应答返回0,没有等待到应答返回1 */
uint8_t IIC_wait_ACK(void)
{
uint8_t t = 200;
SDA_OUT();
IIC_SDA(1);
delay_us(1);
IIC_SCL(0);
delay_us(1);
SDA_IN(); /* 数据发送完后释放数据线,准备接收应答位 */
delay_us(1);
while(READ_SDA) /* 等待IIC应答*/
{
t--;
delay_us(1);
if(t==0)
{
IIC_SCL(0);
return 1;
}
delay_us(1);
}
delay_us(1);
IIC_SCL(1);
delay_us(1);
IIC_SCL(0);
delay_us(1);
return 0;
}
/* 描述:一个字节数据发送函数
* 参数: 无
* 返回值:无 */
void IIC_SendByte(uint8_t byte)
{
uint8_t BitCnt;
SDA_OUT();
IIC_SCL(0);
for(BitCnt=0;BitCnt<8;BitCnt++) /* 要传送的数据长度为8位 */
{
if(byte&0x80) IIC_SDA(1); /* 判断发送位 */
else IIC_SDA(0);
byte<<=1;
delay_us(2);
IIC_SCL(1);
delay_us(2);
IIC_SCL(0);
delay_us(2);
}
}
/* 描述:一个字节数据接收函数
* 参数: 无
* 返回值:接收到的字节数据 */
uint8_t IIC_RcvByte(void)
{
uint8_t retc;
uint8_t BitCnt;
retc=0;
SDA_IN(); /* 设置数据线为输入方式 */
delay_us(1);
for(BitCnt=0;BitCnt<8;BitCnt++)
{
IIC_SCL(0); /* 设置时钟线为低,准备接收数据位 */
delay_us(2);
IIC_SCL(1); /* 设置时钟线为高使数据线上数据有效 */
retc=retc<<1;
if(READ_SDA) retc |=1; /* 读数据位,接收的数据位放入retc中 */
delay_us(1);
}
IIC_SCL(0);
return(retc);
}
soft_i2c.h
文件如下,
#ifndef __SOFT_I2C_H_
#define __SOFT_I2C_H_
#include "gd32f30x.h"
/* 设置IIC_SDA的方向,输入还是输出 */
#define SDA_IN() {GPIO_CTL0(GPIOB)&=~GPIO_MODE_MASK(7);GPIO_CTL0(GPIOB)|=GPIO_MODE_SET(7,0x8);}//PB9输入模式
#define SDA_OUT() {GPIO_CTL0(GPIOB)&=~GPIO_MODE_MASK(7);GPIO_CTL0(GPIOB)|=GPIO_MODE_SET(7,0x3);}//PB9输出模式
/* 设置SCL和SDA输出电平,以及读取SDA电平 */
#define IIC_SCL(n) (n?gpio_bit_set(GPIOB, GPIO_PIN_6):gpio_bit_reset(GPIOB, GPIO_PIN_6))
#define IIC_SDA(n) (n?gpio_bit_set(GPIOB, GPIO_PIN_7):gpio_bit_reset(GPIOB, GPIO_PIN_7))
#define READ_SDA gpio_input_bit_get(GPIOB,GPIO_PIN_7)
void IIC_Init(void); /* 软件模拟IIC引脚初始化 */
void IIC_Start(void); /* 启动I2C总线,即发送I2C起始条件 */
void IIC_Stop(void); /* 结束I2C总线,即发送I2C结束条件 */
void IIC_ACK(void); /* 发送应答 ACK */
void IIC_NACK(void); /* 发送非应答 NACK */
uint8_t IIC_wait_ACK(void); /* 等待ACK */
void IIC_SendByte(uint8_t byte);/* 一个字节数据发送函数 */
uint8_t IIC_RcvByte(void); /* 一个字节数据接收函数 */
#endif
二、编写SHT30应用程序
在【Devices】下新建sht3x.c
和sht3x.h
文件,头文件如下
#ifndef __SHT3X_H_
#define __SHT3X_H_
#include "gd32f30x.h"
#include "systick.h"
uint8_t SHT3x_Init(void); /* 描述:SHT3x初始化函数 */
uint8_t SHT3x_ReadSerialNumber(uint32_t* serialNumber);
uint8_t SHT3x_Get_Humiture_single(double *Tem_val,double *Hum_val); /* 单次获取 */
uint8_t SHT3x_Get_Humiture_periodic(double *Tem_val,double *Hum_val); /* 周期获取 */
/* 枚举SHT3x命令列表 */
typedef enum
{
/* 软件复位命令 */
SOFT_RESET_CMD = 0x30A2,
/* 单次测量模式
命名格式:Repeatability_CS_CMD
CS: Clock stretching */
HIGH_ENABLED_CMD = 0x2C06,
MEDIUM_ENABLED_CMD = 0x2C0D,
LOW_ENABLED_CMD = 0x2C10,
HIGH_DISABLED_CMD = 0x2400,
MEDIUM_DISABLED_CMD = 0x240B,
LOW_DISABLED_CMD = 0x2416,
/* 周期测量模式
命名格式:Repeatability_MPS_CMD
MPS:measurement per second */
HIGH_0_5_CMD = 0x2032,
MEDIUM_0_5_CMD = 0x2024,
LOW_0_5_CMD = 0x202F,
HIGH_1_CMD = 0x2130,
MEDIUM_1_CMD = 0x2126,
LOW_1_CMD = 0x212D,
HIGH_2_CMD = 0x2236,
MEDIUM_2_CMD = 0x2220,
LOW_2_CMD = 0x222B,
HIGH_4_CMD = 0x2334,
MEDIUM_4_CMD = 0x2322,
LOW_4_CMD = 0x2329,
HIGH_10_CMD = 0x2737,
MEDIUM_10_CMD = 0x2721,
LOW_10_CMD = 0x272A,
/* 周期测量模式读取数据命令 */
READOUT_FOR_PERIODIC_MODE = 0xE000,
/* 读取传感器编号命令 */
READ_SERIAL_NUMBER = 0x3780,
} SHT3X_CMD;
#endif
在sht3x.c
文件中,先将读写标志以及SHT30的地址设置好:
#define write 0
#define read 1
uint8_t addr = 0x44; //设置传感器地址
然后就是发送命令和接收数据函数
/* 描述:向SHT30发送一条16bit指令
* 参数cmd:SHT30指令(在SHT30_MODE中枚举定义)
* 返回值:发送成功返回0,发送失败返回1 */
static uint8_t SHT3x_Send_Cmd(SHT3X_CMD cmd)
{
uint8_t cmd_buffer[2];
uint8_t ret;
cmd_buffer[0] = cmd >> 8;
cmd_buffer[1] = cmd;
IIC_SendByte(addr<<1 | write); /* 写7位I2C设备地址加0作为写取位 */
ret = IIC_wait_ACK();
IIC_SendByte(cmd_buffer[0]);
ret |= IIC_wait_ACK();
IIC_SendByte(cmd_buffer[1]);
ret |= IIC_wait_ACK();
return ret;
}
/* 描述:从SHT3x读取数据
* 参数data_len:读取多少个字节数据
* 参数data_arr:读取的数据存放在一个数组里
* 返回值:读取成功返回0,读取失败返回1
*/
static uint8_t SHT3x_Recv_Data(uint8_t data_len, uint8_t* data_arr)
{
uint8_t ret,i;
IIC_SendByte(addr<<1 | read); /* 写7位I2C设备地址加1为读取位 */
ret = IIC_wait_ACK();
if(ret != 0) return 1;
for(i = 0; i < (data_len - 1); i++)
{
data_arr[i]=IIC_RcvByte();
IIC_ACK();
}
data_arr[i]=IIC_RcvByte();
IIC_NACK();
return 0;
}
接下来我们测试一下读取传感器的编号
/* 描述:读取传感器编号
* 参数:存储编号数据的指针
* 返回值:0-读取成功,1-读取失败 */
uint8_t SHT3x_ReadSerialNumber(uint32_t* serialNumber)
{
uint8_t ret = 0;
uint8_t Num_buf[4] = {0xFF,0xFF,0xFF,0xFF};
IIC_Start();
SHT3x_Send_Cmd(READ_SERIAL_NUMBER);
IIC_Stop();
delay_1ms(10); /* 有问题时需要适当延长!!!!!!*/
IIC_Start();
ret = SHT3x_Recv_Data(4,Num_buf);
IIC_Stop();
*serialNumber = ((Num_buf[0] << 24) | (Num_buf[1] << 16) |(Num_buf[2] << 8) |(Num_buf[3]));
if(0xFF == *serialNumber) return 1;
return ret;
}
在main函数中调用,编译下载运行,可以得到其编号:
然后我们将其设置为周期读取模式,周期读取数据
/* 描述:数据CRC校验
* 参数message:需要校验的数据
* 参数initial_value:crc初始值
* 返回值:计算得到的CRC码 */
#define CRC8_POLYNOMIAL 0x131
uint8_t CheckCrc8(uint8_t* const message, uint8_t initial_value)
{
uint8_t remainder; //余数
uint8_t i = 0, j = 0; //循环变量
/* 初始化 */
remainder = initial_value;
for(j = 0; j < 2;j++)
{
remainder ^= message[j];
/* 从最高位开始依次计算 */
for (i = 0; i < 8; i++)
{
if (remainder & 0x80)
remainder = (remainder << 1)^CRC8_POLYNOMIAL;
else
remainder = (remainder << 1);
}
}
/* 返回计算的CRC码 */
return remainder;
}
/* 描述:温湿度数据获取函数,周期读取,注意,需要提前设置周期模式
* 参数Tem_val:存储温度数据的指针, 温度单位为°C
* 参数Hum_val:存储湿度数据的指针, 温度单位为%
* 返回值:0-读取成功,1-读取失败
********************************************************************/
uint8_t SHT3x_Get_Humiture_periodic(double *Tem_val,double *Hum_val)
{
uint8_t ret=0;
uint8_t buff[6];
uint16_t tem,hum;
double Temperature=0;
double Humidity=0;
IIC_Start();
ret = SHT3x_Send_Cmd(READOUT_FOR_PERIODIC_MODE);
IIC_Start();
ret = SHT3x_Recv_Data(6,buff);
IIC_Stop();
/* 校验温度数据和湿度数据是否接收正确 */
if(CheckCrc8(buff, 0xFF) != buff[2] || CheckCrc8(&buff[3], 0xFF) != buff[5])
{
printf("CRC_ERROR,ret = 0x%x\r\n",ret);
return 1;
}
/* 转换温度数据 */
tem = (((uint16_t)buff[0]<<8) | buff[1]);//温度数据拼接
Temperature= (175.0*(double)tem/65535.0-45.0) ; // T = -45 + 175 * tem / (2^16-1)
/* 转换湿度数据 */
hum = (((uint16_t)buff[3]<<8) | buff[4]);//湿度数据拼接
Humidity= (100.0*(double)hum/65535.0); // RH = hum*100 / (2^16-1)
/* 过滤错误数据 */
if((Temperature>=-20)&&(Temperature<=125)&&(Humidity>=0)&&(Humidity<=100))
{
*Tem_val = Temperature;
*Hum_val = Humidity;
return 0;
}
else
return 1;
}
在主函数中,每隔一秒读取一次数据
if( 0 == SHT3x_Init())
printf("SHT3x_Init OK \r\n");
else
printf("SHT3x_Init ERR \r\n");
while(1)
{
delay_1ms(1000);
if(SHT3x_Get_Humiture_periodic(&Tem_val,&Hum_val) == 0)
printf("Tem_val:%f C,\t&Hum_val:%f %%Rh\r\n",Tem_val,Hum_val);
else
printf("Get_Humiture ERR\r\n");
}
编译下载运行,可以得到,读取数据成功
另外,也可以单次读取
/* 描述:温湿度数据获取函数,单次获取
* 参数Tem_val:存储温度数据的指针, 温度单位为°C
* 参数Hum_val:存储湿度数据的指针, 温度单位为%
* 返回值:0-读取成功,1-读取失败
********************************************************************/
uint8_t SHT3x_Get_Humiture_single(double *Tem_val,double *Hum_val)
{
uint8_t ret=0;
uint8_t buff[6];
uint16_t tem,hum;
double Temperature=0;
double Humidity=0;
IIC_Start();
SHT3x_Send_Cmd(HIGH_ENABLED_CMD);
IIC_Stop();
delay_1ms(50);
IIC_Start();
ret = SHT3x_Recv_Data(6,buff);
IIC_Stop();
/* 校验温度数据和湿度数据是否接收正确 */
if(CheckCrc8(buff, 0xFF) != buff[2] || CheckCrc8(&buff[3], 0xFF) != buff[5])
{
printf("CRC_ERROR,ret = 0x%x\r\n",ret);
return 1;
}
/* 转换温度数据 */
tem = (((uint16_t)buff[0]<<8) | buff[1]);//温度数据拼接
Temperature= (175.0*(double)tem/65535.0-45.0) ; // T = -45 + 175 * tem / (2^16-1)
/* 转换湿度数据 */
hum = (((uint16_t)buff[3]<<8) | buff[4]);//湿度数据拼接
Humidity= (100.0*(double)hum/65535.0); // RH = hum*100 / (2^16-1)
/* 过滤错误数据 */
if((Temperature>=-20)&&(Temperature<=125)&&(Humidity>=0)&&(Humidity<=100))
{
*Tem_val = Temperature;
*Hum_val = Humidity;
return 0;
}
else
return 1;
}
三、上传到云端
结合之前的代码,将其温湿度数据上传到EMQ,5秒上报一次:
#include "gd32f30x.h"
#include "systick.h"
#include "usart.h"
#include "led.h"
#include "timer.h"
#include "app_nbiot.h"
#include "soft_i2c.h"
#include "sht3x.h"
extern uint8_t update_flag;
extern int udp_socket;
int main(void)
{
// uint32_t sn;
char res_buf[128];
char data_buf[128];
int times = 0;
double Tem_val,Hum_val;
systick_config(); /* 配置系统时钟 */
LED_init(); /* 初始化 LED */
uart_init(115200); /* 初始化USART0 */
uart1_init(9600); /* 初始化USART1 */
timer5_init(50000,12000); /* 定时5000ms*/
printf("Hello world! \r\n");
while(NB_Start())
{
u1_printf("AT+NRB\r\n"); /* 重启*/
while(USART1_RX_NUM == 0)
;
if(USART1_RX_NUM >0)
{
printf("NBIoT: %s\r\n",rx1_date_buf);
USART1_RX_NUM = 0;
}
}
printf("start OK\r\n");
// IIC_Init();
// if( 0 == SHT3x_ReadSerialNumber(&sn))
// printf("ReadSerialNumber OK sn = 0x%x \r\n",sn);
// else
// printf("ReadSerialNumber ERR \r\n");
if( 0 == SHT3x_Init())
printf("SHT3x_Init OK \r\n");
else
printf("SHT3x_Init ERR \r\n");
while(1)
{
if(update_flag == 1)
{
update_flag = 0;
times++;
LED(times%2);
/* 采集温湿度数据然后上传到EMQ */
if(SHT3x_Get_Humiture_periodic(&Tem_val,&Hum_val) == 0)
{
sprintf(data_buf,"{\"temperature\":%6.2lf ,\"humidity\":%6.2lf }",Tem_val,Hum_val);
NB_Send_data("39.96.35.207", "SHT3x", data_buf);
printf("%s\r\nData send to EMQ...\r\n",data_buf);
}
else
printf("Get_Humiture ERR\r\n");
}
if(USART1_RX_NUM >0)
{
USART1_RX_NUM = 0;
if(Find_string((char *)rx1_date_buf,"+NSONMI:","\r\n",res_buf)>0)
{
u1_printf("AT+NSORF=%d,%d\r\n",udp_socket,res_buf[2]-'0');
}
printf("NBIOT: %s\r\n",rx1_date_buf);
}
if(USART_RX_NUM >0)
{
USART_RX_NUM = 0;
u1_printf("%s",rx0_date_buf);
}
}
}
编译下载运行,可以看到,在EMQ上接收到数据
四、代码
完整代码我存放在码云,可以查看:https://gitee.com/william_william/GD32.git