STM32 программный I2C чтение данных датчика температуры и влажности AM2320

Микроконтроллер STM32 считывает данные с датчика температуры и влажности AM2320 по программному I2C и выводит их на OLED-дисплей 0,96 дюйма.

Я использую плату STM32F103C8T6, а программу написал на стандартной библиотеке ST.

STM32 считывает данные с датчика температуры и влажности SHTC3 по аппаратному I2C: https://blog.zeruns.com/archives/692.html
STM32 считывает данные с датчика температуры и влажности AHT10: https://blog.zeruns.com/archives/693.html

Техническая группа по электронике/микроконтроллерам: 2169025065

Результат работы

Краткое описание протокола I2C

Протокол связи I2C (Inter-Integrated Circuit) был разработан компанией Philips. Он требует минимум выводов, прост в аппаратной реализации, легко масштабируется и не нуждается во внешних приёмопередатчиках (например, преобразователях уровня), как USART или CAN, поэтому сегодня широко используется для обмена между несколькими ИС внутри системы.

I2C имеет всего одну линию данных SDA (Serial Data Line) — последовательную шину данных, по которой информация передаётся побитно; это последовательный полудуплексный интерфейс.
Полудуплекс означает, что связь возможна в обоих направлениях, но не одновременно — передача и приём чередуются. Достаточно одной линии данных.

Протокол условно делят на физический и сетевой уровни.
Физический уровень описывает электрические и механические особенности (аппаратная часть), обеспечивая передачу сырого сигнала.
Сетевой уровень определяет логику обмена: формат кадров, правила упаковки и распаковки данных (программная часть).

Физический уровень I2C

Типичное подключение устройств по шине I2C

  1. Это мультидропная шина — несколько устройств подключены к общим линиям. На одной шине I2C могут находиться несколько мастеров и несколько ведомых.
  2. Для работы достаточно двух линий:
    • SDA — двунаправленная линия данных (Serial Data);
    • SCL — линия синхронизации тактового сигнала (Serial Clock).
  3. Шина подтянута к питанию резисторами. Когда устройство не передаёт, оно переходит в высокоимпеданное состояние. Если все устройства «молчат», резисторы устанавливают высокий логический уровень.
    Для работы по I2C выводы микроконтроллера нужно настроить в режиме open-drain, иначе возможно короткое замыкание.

Более подробно про I2C на STM32: https://url.zeruns.com/JC0Ah

Датчик температуры и влажности AM2320

Общие сведения

AM2320 — цифровой комбинированный датчик температуры и влажности с заводской калибровкой. Используется специализированная технология измерения, обеспечивающая высокую надёжность и долгосрочную стабильность. В состав входят емкостной элемент для измерения влажности, высокоточный температурный датчик и высокопроизводительный микроконтроллер.
Достоинства: компактность, сверхбыстрый отклик, высокая помехоустойчивость, отличное соотношение цена/качество.
Поддерживает два интерфейса: однопроводной и стандартный I2C. В I2C-режиме достаточно подключить датчик к общей шине — дополнительных линий не требуется. В обоих режимах выдаются оцифрованные и температурно-компенсированные значения влажности и температуры, а также CRC-контрольная сумма; дополнительных расчётов пользователь не делает. Режимы легко переключаются. Корпус 4-выводной, но возможны варианты исполнения.

Datasheet AM2320: https://url.zeruns.com/74o6F



Краткие характеристики:

  • Диапазон температур: –40 °C … +80 °C
  • Погрешность температуры: ±0,5 °C
  • Диапазон влажности: 0 % … 99,9 %
  • Погрешность влажности: ±3 %
  • Питающее напряжение: 3,1 В … 5,5 В
  • Интерфейс: I2C или 1-Wire
  • Частота SCL: до 100 кГц

Адрес устройства и коды чтения/записи

При передаче адрес объединяется с битом направления R/W в один байт. Старшие 7 бит — адрес AM2320 (0xB8 >> 1 = 0x5C), младший бит: 0 — запись, 1 — чтение.

  • Запись: 0xB8
  • Чтение: 0xB9

Считывание данных




Полный цикл измерения состоит из трёх шагов:

  1. «Разбудить» датчик
  2. Отправить команду чтения
  3. Принять данные

Подробно:

  1. Wake-up: START → 0xB8 → задержка > 800 мкс → STOP
  2. Команда чтения: START → 0xB8 (адрес + WR) → 0x03 (функция) → 0x00 (старт. адрес) → 0x04 (кол-во рег.) → STOP
  3. Чтение: START → 0xB9 (адрес + RD) → принять 8 байт:
    длина | ст. байт влаж. | мл. байт влаж. | ст. байт темп. | мл.```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.com

}


### AM2320.c

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

/*
Автор блога: https://blog.zeruns.com
WeChat паблик: zeruns-gzh
Bilibili: 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)
/*Когда GPIO STM32 настроен на режим открытого стока, его всё ещё можно читать через регистр входных данных,
то есть он одновременно работает как вход с плавающим потенциалом*/

/**
 * @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 START
 * @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 STOP
 * @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  Ожидание сигнала ACK
 * @param  нет
 * @retval 1 — NACK, 0 — ACK
 */
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 — NACK, 0 — ACK
 * @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); // отправить ACK/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(); // отправить START
	AM2320_I2C_SendByte(AM2320_ADDRESS);
	if (WaitAck()) // проверить ACK
	{
		AM2320### OLED.c

```c
#include "stm32f10x.h"
#include "OLED_Font.h"

/*Конфигурация выводов*/
#define OLED_SCL GPIO_Pin_12
#define OLED_SDA GPIO_Pin_13
#define OLED_W_SCL(x) GPIO_WriteBit(GPIOB, OLED_SCL, (BitAction)(x))
#define OLED_W_SDA(x) GPIO_WriteBit(GPIOB, OLED_SDA, (BitAction)(x))

/*Инициализация выводов*/
void OLED_I2C_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Pin = OLED_SCL;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	GPIO_InitStructure.GPIO_Pin = OLED_SDA;
	GPIO_Init(GPIOB, &GPIO_InitStructure);

	OLED_W_SCL(1);
	OLED_W_SDA(1);
}

/**
 * @brief  I2C старт
 * @param  нет
 * @retval нет
 */
void OLED_I2C_Start(void)
{
	OLED_W_SDA(1);
	OLED_W_SCL(1);
	OLED_W_SDA(0);
	OLED_W_SCL(0);
}

/**
 * @brief  I2C стоп
 * @param  нет
 * @retval нет
 */
void OLED_I2C_Stop(void)
{
	OLED_W_SDA(0);
	OLED_W_SCL(1);
	OLED_W_SDA(1);
}

/**
 * @brief  I2C отправка байта
 * @param  Byte байт для отправки
 * @retval нет
 */
void OLED_I2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i++)
	{
		OLED_W_SDA(Byte & (0x80 >> i));
		OLED_W_SCL(1);
		OLED_W_SCL(0);
	}
	OLED_W_SDA(1); //освободить шину SDA
	OLED_W_SCL(1); //дополнительный такт, без обработки сигнала ACK
	OLED_W_SCL(0);
}

/**
 * @brief  запись команды OLED
 * @param  Command команда для записи
 * @retval нет
 */
void OLED_WriteCommand(uint8_t Command)
{
	OLED_I2C_Start();
	OLED_I2C_SendByte(0x78); //адрес ведомого
	OLED_I2C_SendByte(0x00); //запись команды
	OLED_I2C_SendByte(Command);
	OLED_I2C_Stop();
}

/**
 * @brief  запись данных OLED
 * @param  Data данные для записи
 * @retval нет
 */
void OLED_WriteData(uint8_t Data)
{
	OLED_I2C_Start();
	OLED_I2C_SendByte(0x78); //адрес ведомого
	OLED_I2C_SendByte(0x40); //запись данных
	OLED_I2C_SendByte(Data);
	OLED_I2C_Stop();
}

/**
 * @brief  установка позиции курсора OLED
 * @param  Y координата сверху вниз от левого верхнего угла, диапазон: 0~7
 * @param  X координата слева направо от левого верхнего угла, диапазон: 0~127
 * @retval нет
 */
void OLED_SetCursor(uint8_t Y, uint8_t X)
{
	OLED_WriteCommand(0xB0 | Y);			 //установить позицию Y
	OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4)); //установить младшие 4 бита X
	OLED_WriteCommand(0x00 | (X & 0x0F));		 //установить старшие 4 бита X
}

/**
 * @brief  очистка экрана OLED
 * @param  нет
 * @retval нет
 */
void OLED_Clear(void)
{
	uint8_t i, j;
	for (j = 0; j < 8; j++)
	{
		OLED_SetCursor(j, 0);
		for (i = 0; i < 128; i++)
		{
			OLED_WriteData(0x00);
		}
	}
}

/**
 * @brief  частичная очистка OLED
 * @param  Line номер строки, диапазон: 1~4
 * @param  start начальный столбец, диапазон: 1~16
 * @param  end конечный столбец, диапазон: 1~16
 * @retval нет
 */
void OLED_Clear_Part(uint8_t Line, uint8_t start, uint8_t end)
{
	uint8_t i, Column;
	for (Column = start; Column <= end; Column++)
	{
		OLED_SetCursor((Line - 1) * 2, (Column - 1) * 8); //установить курсор в верхней половине
		for (i = 0; i < 8; i++)
		{
			OLED_WriteData(0x00); //очистить верхнюю половину
		}
		OLED_SetCursor((Line - 1) * 2 + 1, (Column - 1) * 8); //установить курсор в нижней половине
		for (i = 0; i < 8; i++)
		{
			OLED_WriteData(0x00); //очистить нижнюю половину
		}
	}
}

/**
 * @brief  OLED вывод символа
 * @param  Line номер строки, диапазон: 1~4
 * @param  Column номер столбца, диапазон: 1~16
 * @param  Char символ для отображения, диапазон: видимые ASCII-символы
 * @retval нет
 */
void OLED_ShowChar(uint8_t Line, uint8_t Column, char Char)
{
	uint8_t i;
	OLED_SetCursor((Line - 1) * 2, (Column - 1) * 8); //установить курсор в верхней половине
	for (i = 0; i < 8; i++)
	{
		OLED_WriteData(OLED_F8x16[Char - ' '][i]); //отобразить верхнюю половину
	}
	OLED_SetCursor((Line - 1) * 2 + 1, (Column - 1) * 8); //установить курсор в нижней половине
	for (i = 0; i < 8; i++)
	{
		OLED_WriteData(OLED_F8x16[Char - ' '][i + 8]); //отобразить нижнюю половину
	}
}

/**
 * @brief  OLED вывод строки
 * @param  Line начальная строка, диапазон: 1~4
 * @param  Column начальный столбец, диапазон: 1~16
 * @param  String строка для отображения, диапазон: видимые ASCII-символы
 * @retval нет
 */
void OLED_ShowString(uint8_t Line, uint8_t Column, char *String)
{
	uint8_t i;
	for (i = 0; String[i] != '\0'; i++)
	{
		OLED_ShowChar(Line, Column + i, String[i]);
	}
}

/**
 * @brief  OLED степенная функция
 * @retval результат равен X в степени Y
 */
uint32_t OLED_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;
	while (Y--)
	{
		Result *= X;
	}
	return Result;
}

/**
 * @brief  OLED вывод числа (десятичное, положительное)
 * @param  Line начальная строка, диапазон: 1~4
 * @param  Column начальный столбец, диапазон: 1~16
 * @param  Number число для отображения, диапазон: 0~4294967295
 * @param  Length длина числа, диапазон: 1~10
 * @retval нет
 */
void OLED_ShowNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
	uint8_t i;
	for (i = 0; i < Length; i++)
	{
		OLED_ShowChar(Line, Column + i, Number / OLED_Pow(10, Length - i - 1) % 10 + '0');
	}
}

/**
 * @brief  OLED вывод числа (десятичное, со знаком)
 * @param  Line начальная строка, диапазон: 1
1 лайк