数码之家

 找回密码
 立即注册
搜索
查看: 525|回复: 0

[STM] STM32软件I2C读取AM2320温湿度传感器数据

[复制链接]
发表于 2022-12-12 17:47:31 | 显示全部楼层 |阅读模式
STM32单片机使用软件IIC读取AM2320温湿度传感器的数据并显示在0.96寸OLED屏上。
我用的单片机是STM32F103C8T6,程序用的是ST标准库写的。
STM32使用硬件I2C读取SHTC3温湿度传感器:https://blog.zeruns.tech/archives/692.html
STM32单片机读取AHT10温湿度传感器数据:https://blog.zeruns.tech/archives/693.html
实现效果图
I2C协议简介
I2C 通讯协议(Inter-Integrated Circuit)是由 Phiilps 公司开发的,由于它引脚少,硬件实现简单,可扩展性强,不需要 USART、CAN 等通讯协议的外部收发设备(那些电平转化芯片),现在被广泛地使用在系统内多个集成电路(IC)间的通讯。
I2C只有一跟数据总线 SDA(Serial Data Line),串行数据总线,只能一位一位的发送数据,属于串行通信,采用半双工通信
半双工通信:可以实现双向的通信,但不能在两个方向上同时进行,必须轮流交替进行,其实也可以理解成一种可以切换方向的单工通信,同一时刻必须只能一个方向传输,只需一根数据线.
对于I2C通讯协议把它分为物理层和协议层物理层规定通讯系统中具有机械、电子功能部分的特性(硬件部分),确保原始数据在物理媒体的传输。协议层主要规定通讯逻辑,统一收发双方的数据打包、解包标准(软件层面)。
I2C物理层
I2C 通讯设备之间的常用连接方式
(1) 它是一个支持多设备的总线。“总线”指多个设备共用的信号线。在一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。
(2) 一个 I2C 总线只使用两条总线线路,一条双向串行数据线SDA(Serial Data Line ),一条串行时钟线SCL(Serial Data Line )。数据线即用来表示数据,时钟线用于数据收发同步
(3) 总线通过上拉电阻接到电源。当 I2C 设备空闲时会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平
I2C通信时单片机GPIO口必须设置为开漏输出,否则可能会造成短路。
关于更多STM32的I2C相关信息和使用方法可以看这篇文章:https://url.zeruns.tech/JC0Ah
我这里就不详细讲解了。
AM2320温湿度传感器介绍
AM2320 数字温湿度传感器是一款含有己校准数字信号输出的温湿度复合型传感器。采用专用的温湿度采集技术,确保产品具有极高的可靠性与卓越的长期稳定性。传感器包括一个电容式感湿元件和一个高精度集成测温元件,并与一个高性能微处理器相连接。该产品具有品质卓越、超快响应、抗干扰能力强、性价比极高等优点。AM2320 通信方式采用单总线、标准 I2C 两种通信方式。标准单总线接口,使系统集成变得简易快捷。超小的体积、极低的功耗,信号传输距离可达 20 米以上,使其成为各类应用甚至最为苛刻的应用场合的最佳选择。I2C 通信方式采用标准的通信时序,用户可直接挂在 I2C通信总线上,无需额外布线,使用简单。两种通信方式都采用直接输出经温度补偿后的湿度、温度及校验 CRC 等数字信息,用户无需对数字输出进行二次计算,也无需要对湿度进行温度补偿,便可得到准确的温湿度信息。两种通信方式可自由切换,用户可自由选择,使用方便,应该领域广泛。产品为 4 引线,连接方便,特殊封装形式可根据用户需求而提供。
AM2320数据手册下载地址:https://url.zeruns.tech/74o6F
浏览数据手册可以得到一个大概信息:
  • 温度范围:-40℃~80℃
  • 温度误差:±0.5℃
  • 湿度范围:0%~99.9%
  • 湿度误差:±3%
  • 工作电压:3.1v~5.5v
  • 通讯方式:I2C或单总线
  • 时钟频率:100kHz以内
找到如下几个关键信息
设备地址和读写命令
在实际的使用过程中,AM2320的设备地址需要与读写数据/命令方向位组成一个字节同时发送,字节的最低位为读写数据/命令方向位,高7位是AM2320的设备地址。
如果要通过I2C写数据或命令给AM2320,在I2C起始信号之后,需要发送“1011 1000”,即0xB8给AM2320,除了通过高7位“1011 100”的设备地址寻址还通过最低位“0”通知AM2320接下来是写数据或命令操作。
如果要通过I2C读取AM2320中的数据,在I2C起始信号之后,需要发送“1011 1001”,即0xB9给AM2320,除了通过高7位“1011 100”的设备地址寻址还通过最低位“1”通知AM2320接下来是读取数据的操作。
简单来说就是,0xB8表示写数据,0xB9表示读数据。
读取温湿度数据



从数据手册可知,一个读取周期包概括三个步骤:
  • 唤醒传感器
  • 发送读指令
  • 读返回数据
总结如下:
  • 唤醒传感器:起始信号+发送0xB8+等待(>800us)+停止信号
  • 发送读指令:START+发送0xB8(SLA)+0x03(功能码)+0x00(起始地址)+0x04(寄存器长度)+STOP
  • 接收数据:发送读取指令(0xB9),连续接收8个字节数据。接收到的数据分别为 数据长度+湿度高位+湿度低位+温度高位+温度低位+CRC校验码低字节+CRC校验码高字节
  • 对接收到的数据进行转换处理。
数据的计算
由AM2320数据手册可知

例如:采集到的湿度数值是0x01F4,换算成十进制是500。
则:湿度 = 500 / 10 = 50.0 (单位:%)
采集到的温度数值是0x00FA,换算成十进制是250。
则:温度 = 250 / 10 = 25.0 (单位:℃)
需要用的元件程序
这里就放出main.c、AM2320.c和OLED.c这三个主要的代码,其他的请下载下面链接的压缩包。
完整工程文件:https://url.zeruns.tech/AM2320
AM2320和OLED模块的 SCL接PB12,SDA接PB13。 如果AM2320单独用别的IO口那要记得接上拉电阻,5KΩ左右就行。
使用VSCode代替Keil实现STM32和51单片机的开发:https://blog.zeruns.tech/archives/690.html
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "AM2320.h"
#include "IWDG.h"

int main(void)
{
        IWDG_Configuration(); //初始化看门狗
        AM2320_I2C_Init();
        OLED_Init();

        OLED_ShowString(1, 1, "T:");
        OLED_ShowString(2, 1, "H:");

        uint16_t i = 0;
        uint16_t err_count = 0;

        while (1)
        {
                OLED_ShowNum(4, 1, i, 5);
                float Temp, Hum; //声明变量存放温湿度数据

                if (ReadAM2320(&Hum, &Temp)) //读取温湿度数据
                {
                        if (Temp >= 0)
                        {
                                char String[10];
                                sprintf(String, "+%.2fC", Temp); //格式化字符串输出到字符串变量
                                OLED_ShowString(1, 3, String);         //显示温度

                                sprintf(String, " %.2f%%", Hum); //格式化字符串输出到字符串变量
                                OLED_ShowString(2, 3, String);         //显示湿度
                        }
                        else
                        {
                                char String[10];
                                sprintf(String, "-%.2fC", Temp); //格式化字符串输出到字符串变量
                                OLED_ShowString(1, 3, String);         //显示温度

                                sprintf(String, " %.2f%%", Hum); //格式化字符串输出到字符串变量
                                OLED_ShowString(2, 3, String);         //显示湿度
                        }
                }
                else
                {
                        err_count++;
                        OLED_ShowNum(3, 1, err_count, 5); //显示错误次数计数
                }
                Delay_ms(100);
                i++;
                if (i >= 99999)
                        i = 0;
                if (err_count >= 99999)
                        err_count = 0;
                IWDG_FeedDog(); //喂狗(看门狗,超过1秒没有执行喂狗则自动复位)
        }
        // blog.zeruns.tech
}


AM2320.c

#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"

/*
作者博客:https://blog.zeruns.tech
微信公众号:zeruns-gzh
B站主页:https://space.bilibili.com/8320520
*/

/*AM2320地址*/
#define AM2320_ADDRESS 0xB8

/*引脚配置*/
#define AM2320_SCL GPIO_Pin_12
#define AM2320_SDA GPIO_Pin_13
#define AM2320_W_SCL(x) GPIO_WriteBit(GPIOB, AM2320_SCL, (BitAction)(x))
#define AM2320_W_SDA(x) GPIO_WriteBit(GPIOB, AM2320_SDA, (BitAction)(x))
#define AM2320_R_SDA() GPIO_ReadInputDataBit(GPIOB, AM2320_SDA)
#define AM2320_R_SCL() GPIO_ReadInputDataBit(GPIOB, AM2320_SCL)
/*当 STM32 的 GPIO 配置成开漏输出模式时,它仍然可以通过读取
GPIO 的输入数据寄存器获取外部对引脚的输入电平,也就是说它同时具有浮空输入模式的功能*/

/**
* @brief  CRC校验计算
* @param  *ptr 要计算的字节数据(以数组变量形式存储)
* @param  len  要计算的字节个数(数组长度)
* @retval CRC校验码
*/
unsigned short CRC16(unsigned char *ptr, unsigned char len)
{
        unsigned short crc = 0xFFFF;
        unsigned char i;
        while (len--)
        {
                crc ^= *ptr++;
                for (i = 0; i < 8; i++)
                {
                        if (crc & 0x01)
                        {
                                crc >>= 1;
                                crc ^= 0xA001;
                        }
                        else
                        {
                                crc >>= 1;
                        }
                }
        }
        return crc;
}

/**
* @brief  I2C开始
* @param  无
* @retval 无
*/
void AM2320_I2C_Start(void)
{
        AM2320_W_SDA(1);
        Delay_us(2); //延时2微秒
        AM2320_W_SCL(1);
        Delay_us(4);
        AM2320_W_SDA(0);
        Delay_us(3);
        AM2320_W_SCL(0);
        Delay_us(5);
}

/**
* @brief  I2C停止
* @param  无
* @retval 无
*/
void AM2320_I2C_Stop(void)
{
        AM2320_W_SDA(0);
        Delay_us(3);
        AM2320_W_SCL(1);
        Delay_us(4);
        AM2320_W_SDA(1);
        Delay_us(4);
}

/**
* @brief  I2C发送一个字节
* @param  Byte 要发送的一个字节
* @retval 无
*/
void AM2320_I2C_SendByte(uint8_t Byte)
{
        uint8_t i;
        for (i = 0; i < 8; i++)
        {
                AM2320_W_SDA((Byte << i) & 0x80);
                AM2320_W_SCL(1);
                Delay_us(4);
                AM2320_W_SCL(0);
                Delay_us(5);
        }
        AM2320_W_SDA(1); //释放SDA总线
}

/**
* @brief  等待应答信号
* @param  无
* @retval 1-非应答信号,0-应答信号
*/
uint8_t WaitAck(void)
{
        uint8_t ret;

        AM2320_W_SCL(1);
        Delay_us(4);
        if (AM2320_R_SDA())
        {
                ret = 1;
        }
        else
        {
                ret = 0;
        }
        AM2320_W_SCL(0);
        Delay_us(5);
        return ret;
}

/**
* @brief  I2C读取一个字节
* @param  NACK 1-非应答信号,0-应答信号
* @retval 读取到的字节数据
*/
uint8_t AM2320_I2C_ReadByte(uint8_t NACK)
{
        uint8_t i, Byte = 0;
        AM2320_W_SDA(1); //释放SDA总线
        for (i = 0; i < 8; i++)
        {
                AM2320_W_SCL(1);
                Delay_us(4);
                Byte = Byte | (AM2320_R_SDA() << (7 - i));
                AM2320_W_SCL(0);
                Delay_us(5);
        }
        AM2320_W_SDA(NACK); //发送应答/非应答信号
        AM2320_W_SCL(1);
        Delay_us(4);
        AM2320_W_SCL(0);
        Delay_us(5);
        AM2320_W_SDA(1); //释放SDA总线
        return Byte;
}

/*唤醒传感器*/
void AM2320_Wake(void)
{
        AM2320_I2C_Start();
        AM2320_I2C_SendByte(AM2320_ADDRESS);
        WaitAck();
        Delay_us(1000); //延时1000微秒
        AM2320_I2C_Stop();
}

/*引脚初始化*/
void AM2320_I2C_Init(void)
{
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB时钟

        GPIO_InitTypeDef GPIO_InitStructure;                         //定义结构体配置GPIO
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; //开漏输出模式
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_InitStructure.GPIO_Pin = AM2320_SCL;
        GPIO_Init(GPIOB, &GPIO_InitStructure);
        GPIO_InitStructure.GPIO_Pin = AM2320_SDA;
        GPIO_Init(GPIOB, &GPIO_InitStructure);

        AM2320_W_SCL(1);
        AM2320_W_SDA(1);
        AM2320_Wake(); //唤醒传感器
}

/**
* @brief  读取AM2320数据
* @param  *Hum 湿度
* @param  *Temp 温度
* @retval 1 - 读取成功;0 - 读取失败
*/
uint8_t ReadAM2320(float *Hum, float *Temp)
{
        uint8_t Data[8];

        AM2320_I2C_Start(); //发送起始信号
        AM2320_I2C_SendByte(AM2320_ADDRESS);
        if (WaitAck()) //判断应答信号
        {
                AM2320_I2C_Stop(); //发送停止信号
                Delay_us(50);
                //再尝试读取一次
                AM2320_I2C_Start(); //发送起始信号
                AM2320_I2C_SendByte(AM2320_ADDRESS);
                if (WaitAck()) //判断应答信号
                {
                        Delay_us(20);
                        AM2320_I2C_Stop(); //发送停止信号
                        return 0;
                }
                else
                {
                        Delay_us(20);//这里AM2320会莫名其妙地拉低SCL一段时间导致发送的数据出错,所以要延时20微秒等这段时间过去了AM2320释放SCL再继续
                        AM2320_I2C_SendByte(0x03); //发送功能码
                        WaitAck();                                   //等待应答信号
                        AM2320_I2C_SendByte(0x00); //发送要读取的寄存器起始地址
                        WaitAck();                                   //等待应答信号
                        AM2320_I2C_SendByte(0x04); //发送要读取的寄存器长度
                        WaitAck();                                   //等待应答信号
                        Delay_us(20);//这里AM2320会莫名其妙地拉低SCL一段时间导致停止信号发送失败,所以延时20微秒等这段时间过去了AM2320释放SCL再继续
                        AM2320_I2C_Stop();                   //发送停止信号
                }
        }
        else
        {
                Delay_us(20);//这里AM2320会莫名其妙地拉低SCL一段时间导致发送的数据出错,所以要延时20微秒等这段时间过去了AM2320释放SCL再继续
                AM2320_I2C_SendByte(0x03); //发送功能码
                WaitAck();                                   //等待应答信号
                AM2320_I2C_SendByte(0x00); //发送要读取的寄存器起始地址
                WaitAck();                                   //等待应答信号
                AM2320_I2C_SendByte(0x04); //发送要读取的寄存器长度
                WaitAck();                                   //等待应答信号
                Delay_us(20);//这里AM2320会莫名其妙地拉低SCL一段时间导致停止信号发送失败,所以延时20微秒等这段时间过去了AM2320释放SCL再继续
                AM2320_I2C_Stop();                   //发送停止信号
        }

        Delay_ms(2); //延时2毫秒

        AM2320_I2C_Start();
        AM2320_I2C_SendByte(AM2320_ADDRESS | 0x01); //发送读取指令
        WaitAck();
        Delay_us(35);
        uint8_t i;
        for (i = 0; i < 8; i++)
        {
                if (i != 7)
                {
                        Data = AM2320_I2C_ReadByte(0);
                }
                else
                {
                        Data = AM2320_I2C_ReadByte(1); //读取最后一个字节时发送非应答信号
                }
        }
        AM2320_I2C_Stop();

        if (CRC16(Data, 6) == (Data[6] | (Data[7] << 8))) //校验数据
        {
                *Hum = ((((uint16_t)Data[2]) << 8) | Data[3]) / 10.0; //计算湿度数据
                if (Data[4] >> 7)                                                                          //判断温度数值是否为负
                {
                        *Temp = ((((uint16_t)(Data[4] && 0x7F) << 8)) | Data[5]) / -10.0; //计算负温度
                }
                else
                {
                        *Temp = ((((uint16_t)Data[4]) << 8) | Data[5]) / 10.0; //计算正温度
                }
                return 1;
        }
        return 0;
}


时序图
实际运行的AM2320发送指令和读取数据的时序图。



本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

APP|手机版|小黑屋|关于我们|联系我们|法律条款|技术知识分享平台

闽公网安备35020502000485号

闽ICP备2021002735号-2

GMT+8, 2024-4-25 16:42 , Processed in 0.171601 second(s), 10 queries , Redis On.

Powered by Discuz!

© 2006-2023 smzj.net

快速回复 返回顶部 返回列表