Микроконтроллер 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
- Это мультидропная шина — несколько устройств подключены к общим линиям. На одной шине I2C могут находиться несколько мастеров и несколько ведомых.
- Для работы достаточно двух линий:
- SDA — двунаправленная линия данных (Serial Data);
- SCL — линия синхронизации тактового сигнала (Serial Clock).
- Шина подтянута к питанию резисторами. Когда устройство не передаёт, оно переходит в высокоимпеданное состояние. Если все устройства «молчат», резисторы устанавливают высокий логический уровень.
Для работы по 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
Считывание данных
Полный цикл измерения состоит из трёх шагов:
- «Разбудить» датчик
- Отправить команду чтения
- Принять данные
Подробно:
- Wake-up: START → 0xB8 → задержка > 800 мкс → STOP
- Команда чтения: START → 0xB8 (адрес + WR) → 0x03 (функция) → 0x00 (старт. адрес) → 0x04 (кол-во рег.) → STOP
- Чтение: 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









