1. Introduction to I2C
IIC (Inter-Integrated Circuit) is a synchronous, half-duplex, two-wire serial communication bus proposed by Philips, mainly used for short-distance communication between chips, such as between MCU and EEPROM, RTC, OLED screens, MPU6050 sensors, and other modules.
Main features are as follows:Two-wire communication:SCL (clock line) and SDA (data line)Supports multiple master/slave communicationsUnique addressing:Each slave device has a unique 7-bit address2. Configuring IIC in STM32CubeMXOpen STM32CubeMX, first select Connectivity, then click on I2C1 in the list, and in the I2C box on the right, select I2C, keeping other settings as default, and click to generate code.
In the project directory, create an iic directory under Application, and then create iic.c and iic.h files.
Open Keil and add iic.c to the project, while also adding the header file directory for iic.
3. Overview of IIC Master Blocking Mode Send/Receive Functions
Overview of IIC Master Blocking Receive Function
/** * @brief Receive a specified amount of data in blocking mode as a master. * @param hi2c Pointer to the I2C_HandleTypeDef structure that contains the configuration information for the specified I2C. * @param DevAddress Target device address: the 7-bit address value provided in the device's datasheet, which needs to be left-shifted by one before calling this interface. * @param pData Pointer to the data buffer (for storing received data) * @param Size Amount of data to receive (in bytes) * @param Timeout Timeout duration (in milliseconds) * @retval HAL status (HAL_OK, HAL_ERROR, HAL_BUSY, HAL_TIMEOUT) */HAL_StatusTypeDef HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
Overview of IIC Master Blocking Send Function
/** * @brief Send a specified amount of data in blocking mode as a master. * @param hi2c Pointer to the I2C_HandleTypeDef structure that contains the configuration information for the specified I2C. * @param DevAddress Target device address: the 7-bit address value provided in the device's datasheet, which needs to be left-shifted by one before calling this interface. * @param pData Pointer to the data buffer (pointing to the data to be sent) * @param Size Length of data to send (in bytes) * @param Timeout Timeout duration (in milliseconds) * @retval HAL status (e.g., HAL_OK, HAL_ERROR, HAL_BUSY, or HAL_TIMEOUT) */HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
4. Practice: Reading Data from Temperature and Humidity SensorThis time we are using the SHT40.

The SHT40 is a temperature and humidity sensor launched by Sensirion from Switzerland, mainly using I2C for data reading.
Slave 7-bit address: 0x44 (default)
Supports standard mode (100kHz) and fast mode (400kHz)
No interrupt pin, can only use master polling read method
All measurement commands are 1 byte, and the read returns 6 bytes: temperature 2 bytes + CRC + humidity 2 bytes + CRC
Read Command
This practice uses the 0xFD high-precision read command.
Note:The I2C address must be left-shifted by one to be used with the STM32 HAL interface.
Wiring method: SHT40 DATA connects to STM32 IIC1 SDA (PB7), CLK connects to STM32 IIC1 SCL (PB6).
The calculation formulas are as follows:
Temperature
T = -45 + 175 * (raw_T / 65535.0)
Humidity
RH = 100 * (raw_RH / 65535.0)
The implementation code is as follows:
iic.c
#include <stdio.h>#include <string.h>#include <stdint.h>#include "iic.h"#define SHT40_ADDR (0x44 << 1) // SHT40 I2C address#define CMD_MEASURE_HIGH_PRECISION 0xFD // Temperature read commanduint8_t sht40_buf[6] = {0}; // Data returned by SHT40void SHT40_Read_TempHumi(float *temperature, float *humidity) { HAL_I2C_Master_Transmit(&hi2c1, SHT40_ADDR, (uint8_t[]){CMD_MEASURE_HIGH_PRECISION}, 1, HAL_MAX_DELAY); HAL_Delay(10); // Must wait for measurement to complete HAL_I2C_Master_Receive(&hi2c1, SHT40_ADDR, sht40_buf, 6, HAL_MAX_DELAY); uint16_t raw_temp = (sht40_buf[0] << 8) | sht40_buf[1]; uint16_t raw_humi = (sht40_buf[3] << 8) | sht40_buf[4]; *temperature = -45 + 175 * ((float)raw_temp / 65535.0); *humidity = 100 * ((float)raw_humi / 65535.0);}void get_tempHumi(){ float temperature = 0.0f; float humidity = 0.0f; SHT40_Read_TempHumi(&temperature, &humidity); IIC_LOG_INFO("Temperature: %.2f ℃, Humidity: %.2f %%", temperature, humidity);}
iic.h
#ifndef __IIC__HEAD__#define __IIC__HEAD__#include "bsp_log.h"#include "i2c.h"#define MODE_NAME "IIC"#define IIC_LOG_INFO(fmt, ...) BSP_LOG(LOG_INFO,"%s %s-%d "fmt,MODE_NAME,__FUNCTION__,__LINE__, ##__VA_ARGS__)#define IIC_LOG_DEBUG(fmt, ...) BSP_LOG(DEBUG, "%s %s-%d "fmt,MODE_NAME,__FUNCTION__, __LINE__,##__VA_ARGS__)#define IIC_LOG_ERROR(fmt, ...) BSP_LOG(ERROR, "%s %s-%d "fmt,MODE_NAME,__FUNCTION__, __LINE__,##__VA_ARGS__)void get_tempHumi();#endif
Key Code Analysis:
uint16_t raw_temp = (sht40_buf[0]<<8)|sht40_buf[1]; uint16_t raw_humi = (sht40_buf[3] << 8) | sht40_buf[4];
The order of the returned data is as follows:
Byte0: Temperature high byte (MSB)
Byte1: Temperature low byte (LSB)
Byte2: Temperature CRC check byte
Byte3: Humidity high byte (MSB)
Byte4: Humidity low byte (LSB)
Byte5: Humidity CRC check byte
By left-shifting the high byte by 8 bits or combining it with the low byte, we can obtain a complete 16-bit raw data.
Using 8421 code to illustrate, 1 bit represents 4 bits of binary.
High byte buf[0] is 69, low byte buf[1] is 5B.
69 left-shifted by 8 bits is: 6900|5b, which is 695b.
According to the formula, the temperature can be calculated as: 27.02 degrees Celsius.
5. Correction
Due to the lack of implementation of floating-point formatting output in the core formatting function of the serial output function bsp_log.c in the previous article, this article supplements it. Replace the following code in bsp_log.c.
bsp_log.c (supports floating-point formatted output)
#include <stdio.h>#include <string.h>#include <stdint.h>#include <stdarg.h>#include <stdbool.h>#include "usart.h"#include "bsp_log.h"static int print_number(putc_t putcf, unsigned int num, int base, bool upper, int width, char pad){ char buf[32]; const char *digits = upper ? "0123456789ABCDEF" : "0123456789abcdef"; int i = 0, count = 0; do { buf[i++] = digits[num % base]; num /= base; } while (num); while (i < width) { if (putcf(pad) == EOF) return -1; count++; width--; } while (i--) { if (putcf(buf[i]) == EOF) return -1; count++; } return count;}static int print_float(putc_t putcf, double val, int precision){ int cnt = 0; if (val < 0) { if (putcf('-') == EOF) return -1; cnt++; val = -val; } int int_part = (int)val; double frac_part = val - int_part; cnt += print_number(putcf, int_part, 10, false, 0, ' '); if (putcf('.') == EOF) return -1; cnt++; for (int i = 0; i < precision; i++) frac_part *= 10; cnt += print_number(putcf, (int)(frac_part + 0.5), 10, false, precision, '0'); return cnt;}static int evprintf(putc_t putcf, const char *fmt, va_list ap){ int cnt = 0; if (!putcf || !fmt) return -1; while (*fmt) { if (*fmt != '%') { if (putcf(*fmt++) == EOF) return -1; cnt++; continue; } fmt++; // skip '%' // --- Format parsing --- char pad = ' '; int width = 0; int precision = 6; // default precision for float bool long_flag = false; // Zero padding if (*fmt == '0') { pad = '0'; fmt++; } // Width while (*fmt >= '0' && *fmt <= '9') { width = width * 10 + (*fmt++ - '0'); } // Precision (e.g., %.2f) if (*fmt == '.') { fmt++; precision = 0; while (*fmt >= '0' && *fmt <= '9') { precision = precision * 10 + (*fmt++ - '0'); } } // Long integer if (*fmt == 'l') { long_flag = true; fmt++; } // Format specifier switch (*fmt++) { case 'c': { char c = (char)va_arg(ap, int); if (putcf(c) == EOF) return -1; cnt++; break; } case 's': { char *s = va_arg(ap, char*); while (*s) { if (putcf(*s++) == EOF) return -1; cnt++; } break; } case 'd': { int n = long_flag ? va_arg(ap, long) : va_arg(ap, int); if (n < 0) { if (putcf('-') == EOF) return -1; cnt++; n = -n; } int ncnt = print_number(putcf, (unsigned int)n, 10, false, width, pad); if (ncnt < 0) return -1; cnt += ncnt; break; } case 'u': { unsigned int n = long_flag ? va_arg(ap, unsigned long) : va_arg(ap, unsigned int); int ncnt = print_number(putcf, n, 10, false, width, pad); if (ncnt < 0) return -1; cnt += ncnt; break; } case 'x': case 'X': { bool upper = (*(fmt - 1) == 'X'); unsigned int n = long_flag ? va_arg(ap, unsigned long) : va_arg(ap, unsigned int); int ncnt = print_number(putcf, n, 16, upper, width, pad); if (ncnt < 0) return -1; cnt += ncnt; break; } case 'f': { double f = va_arg(ap, double); int ncnt = print_float(putcf, f, precision); if (ncnt < 0) return -1; cnt += ncnt; break; } case '%': { if (putcf('%') == EOF) return -1; cnt++; break; } default: { if (putcf('%') == EOF || putcf(*(fmt - 1)) == EOF) return -1; cnt += 2; break; } } } return cnt;}/** * @brief Use a custom putc function to output formatted strings * * This function formats according to the specified format and parameters, and outputs character by character through the provided putc function. * * @param putcf Character output function pointer (putc_t). * @param fmt Format string (similar to printf). * @param ... Additional parameters to format. * @return Number of characters output. */int eprintf(putc_t putcf, const char *fmt, ...){ va_list ap; int len; va_start(ap, fmt); len = evprintf(putcf, fmt, ap); va_end(ap); return len;}
Finally, call the get_tempHumi() function in man.c
Delay reading once every second, the effect is as follows
If you find this article useful, please give it a follow~If you want the SHT40 datasheet, reply with SHT40 in the background.