小熊派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_SDAgpio_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_CMDCS: 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_CMDMPS: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 1uint8_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 0x131uint8_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;elseremainder = (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;}elsereturn 1;}
在主函数中,每隔一秒读取一次数据
if( 0 == SHT3x_Init())printf("SHT3x_Init OK \r\n");elseprintf("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);elseprintf("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;}elsereturn 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");elseprintf("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);}elseprintf("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上接收到数据
四、代码
完整代码我存放在码云,可以查看:/william_william/GD32.git