简介
在最初拿到这块开发板时,我想用它驱动一些 I2C 传感器,但查看原理图后发现板上的硬件 I2C 接口已被占用,因此无法直接使用。这在使用体验上不够友好。在开发板设计阶段,建议将硬件 I2C 的对应引脚预留出来,以便用户扩展 I2C 设备。所以在最开始的时候我便放置起来的。 后来通过查看官方的教程手册进行进行确认。发现驱动OLED的代码也是通过软件I2C实现的。这样便是真的确定了没有I2C Pin的引出。 因此对于I2C的驱动只能使用软件的方式。本文章将会介绍如何使用软件I2C来驱动SHT30 传感器。
软件I2C的驱动库并不是我编写的。我参考了SoftI2C_HAL_Lib 库进行实现。

我主要的修改点是修改了这个库的配置文件,对于延时的控制我使用了DWT (Data Watchpoint and Trace)进行实现的。其核心代码如下所示。
// delay_us.c
#include "delay_us.h"
static uint32_t us_ticks; // 每微秒计数
void Delay_us_Init(void)
{
// 计算每微秒的计数,系统时钟 HCLK
us_ticks = HAL_RCC_GetHCLKFreq() / 1000000;
}
// 微秒延时
void Delay_us(uint32_t us)
{
uint32_t start = DWT->CYCCNT;
uint32_t delay_ticks = us * us_ticks;
while ((DWT->CYCCNT - start) < delay_ticks);
}
// DWT 初始化函数(只需调用一次)
void Delay_us_DWT_Init(void)
{
if (!(CoreDebug->DEMCR & CoreDebug_DEMCR_TRCENA_Msk))
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 启用计数器
}
然后在它原本软件I2C延时的时候便可以使用DWT进行计数。
/**
* @file softi2c_conf.h
* @author Myth
* @version 0.1
* @date 2021.10.12
* @brief STM32 SoftI2C Library Config File
*/
#ifndef __SOFTI2C_CONF_H
#define __SOFTI2C_CONF_H
// Set your HAL Library here.
#include "stm32f1xx_hal.h"
#include "stm32f1xx_hal_tim.h"
// Set your owm Delay_us function here.
#include "delay_us.h"
#define SoftI2C_Delay_us(__time__) Delay_us(__time__)
#endif
所以这样就可以实现高精度的延时控制来避免了I2C通讯失败。 我这里是没有注意,最初的时候使用的HAL的delay进行实现的,导致I2C通讯失败
使用的时候只需要在主程序中对方法进行初始化即可。
Delay_us_DWT_Init(); // 初始化 DWT 计数器
Delay_us_Init();
驱动SHT30
SHT30的驱动起来比较简单,具体的命令可以参考数据手册通讯的部分,只需要注意一点即找到对应的I2C地址即可。不同的SHT30可能有不同的地址,可以使用I2C scanner进行确认。
void I2C_Scan(void)
{
printf("Scanning I2C bus...\r\n");
uint8_t ack;
uint8_t found = 0;
for (uint8_t addr = 0x03; addr <= 0x77; addr++)
{
printf("Trying address 0x%02X ... ", addr);
SoftI2C_Start(&i2c);
SoftI2C_WriteByte(&i2c, (addr << 1) | 0); // 写模式
ack = SoftI2C_WaitAck(&i2c);
SoftI2C_Stop(&i2c);
if (ack == 0)
{
printf("Device found (ACK=0)\r\n");
found = 1;
}
else
{
printf("No device (ACK=1)\r\n");
}
}
if (!found)
{
printf("No I2C devices detected!\r\n");
}
else
{
printf("I2C scan done.\r\n");
}
}
SHT30.H
/**
* @file sht30.h
* @brief STM32 SoftI2C Library for SHT30 Sensor
*/
#ifndef __SHT30_H
#define __SHT30_H
#include "softi2c.h"
#include "stdint.h"
// SHT30 默认 I2C 地址
#define SHT30_ADDR 0x44
// 单次高精度测量命令 (高重复率)
#define SHT30_CMD_MEAS_HIGHREP 0x2400
/**
* @brief 检测 SHT30 是否存在
* @retval 1: 存在 0: 不存在
*/
uint8_t SHT30_Check(void);
/**
* @brief 向 SHT30 发送测量命令
*/
void SHT30_StartMeasure(SoftI2C_TypeDef *i2c);
/**
* @brief 读取 SHT30 测量值
* @param temperature: 温度值 (℃)
* @param humidity: 湿度值 (%RH)
* @retval 0: 成功 1: 读取失败/CRC错误
*/
uint8_t SHT30_Read(SoftI2C_TypeDef *i2c, float *temperature, float *humidity);
#endif
SHT30.C
/**
* @file sht30.c
* @brief STM32 SoftI2C Library for SHT30 with debug
*/
#include "sht30.h"
#include "stdio.h"
extern SoftI2C_TypeDef i2c;
#define SHT30_ADDR 0x44 // 默认 I2C 地址
// 单次高精度测量命令
#define SHT30_CMD_MEAS_HIGHREP 0x2400
// CRC 校验
static uint8_t SHT30_CalcCRC(uint8_t *data, uint8_t len)
{
uint8_t crc = 0xFF;
for (uint8_t i = 0; i < len; i++)
{
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++)
{
if (crc & 0x80)
crc = (crc << 1) ^ 0x31; // 多项式 0x31
else
crc <<= 1;
}
}
return crc;
}
// -------------------- 检测 SHT30 --------------------
uint8_t SHT30_Check()
{
uint8_t ack;
SoftI2C_Start(&i2c);
SoftI2C_WriteByte(&i2c, (SHT30_ADDR << 1) | 0); // 写模式
ack = SoftI2C_WaitAck(&i2c);
SoftI2C_Stop(&i2c);
if (ack == 0) {
printf("SHT30 ACK detected!\r\n");
return 1; // 应答
} else {
printf("SHT30 not responding!\r\n");
return 0;
}
}
// -------------------- 发送测量命令 --------------------
void SHT30_StartMeasure(SoftI2C_TypeDef *i2c)
{
SoftI2C_Start(i2c);
SoftI2C_WriteByte(i2c, (SHT30_ADDR << 1) | 0);
if (SoftI2C_WaitAck(i2c)) {
SoftI2C_Stop(i2c);
printf("SHT30 StartMeasure ACK failed!\r\n");
return;
}
SoftI2C_WriteByte(i2c, (SHT30_CMD_MEAS_HIGHREP >> 8) & 0xFF);
SoftI2C_WaitAck(i2c);
SoftI2C_WriteByte(i2c, SHT30_CMD_MEAS_HIGHREP & 0xFF);
SoftI2C_WaitAck(i2c);
SoftI2C_Stop(i2c);
printf("SHT30 Measurement command sent: 0x%02X 0x%02X\r\n",
(SHT30_CMD_MEAS_HIGHREP >> 8) & 0xFF,
SHT30_CMD_MEAS_HIGHREP & 0xFF);
}
// -------------------- 读取测量数据 --------------------
uint8_t SHT30_Read(SoftI2C_TypeDef *i2c, float *temperature, float *humidity)
{
uint8_t buf[6] = {0};
// 读取 6 字节
SoftI2C_Start(i2c);
SoftI2C_WriteByte(i2c, (SHT30_ADDR << 1) | 1); // 读模式
SoftI2C_WaitAck(i2c);
for (uint8_t i = 0; i < 6; i++) {
buf[i] = SoftI2C_ReadByte(i2c);
if (i != 5) SoftI2C_Ack(i2c);
else SoftI2C_NAck(i2c);
}
SoftI2C_Stop(i2c);
printf("SHT30 ReadBytes: ");
for (uint8_t i = 0; i < 6; i++) printf("%02X ", buf[i]);
printf("\r\n");
// CRC 校验
if (SHT30_CalcCRC(buf,2) != buf[2] || SHT30_CalcCRC(buf+3,2) != buf[5]) {
printf("SHT30 CRC error!\r\n");
return 1;
}
// 温度和湿度计算
uint16_t raw_temp = (buf[0] << 8) | buf[1];
uint16_t raw_hum = (buf[3] << 8) | buf[4];
*temperature = -45 + 175 * ((float)raw_temp / 65535.0f);
*humidity = 100 * ((float)raw_hum / 65535.0f);
printf("SHT30 Converted: Temp %.2f C Humidity %.2f %%\r\n", *temperature, *humidity);
return 0;
}
main.c
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2025 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "adc.h"
#include "usart.h"
#include "gpio.h"
#include "stdio.h"
#include "softi2c.h"
#include "softi2c_conf.h"
#include "sht30.h"
#include "delay_us.h"
SoftI2C_TypeDef i2c;
void SystemClock_Config(void);
int __io_putchar(int ch) {
HAL_UART_Transmit(&huart1, (uint8_t *) &ch, 1, HAL_MAX_DELAY);
return ch;
}
void I2C_Scan(void)
{
printf("Scanning I2C bus...\r\n");
uint8_t ack;
uint8_t found = 0;
for (uint8_t addr = 0x03; addr <= 0x77; addr++)
{
printf("Trying address 0x%02X ... ", addr);
SoftI2C_Start(&i2c);
SoftI2C_WriteByte(&i2c, (addr << 1) | 0); // 写模式
ack = SoftI2C_WaitAck(&i2c);
SoftI2C_Stop(&i2c);
if (ack == 0)
{
printf("Device found (ACK=0)\r\n");
found = 1;
}
else
{
printf("No device (ACK=1)\r\n");
}
}
if (!found)
{
printf("No I2C devices detected!\r\n");
}
else
{
printf("I2C scan done.\r\n");
}
}
int main(void)
{
float temperature, humidity;
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
Delay_us_DWT_Init(); // 初始化 DWT 计数器
Delay_us_Init();
// 初始化 SoftI2C
i2c.SCL_GPIO = GPIOB;
i2c.SCL_Pin = GPIO_PIN_4;
i2c.SDA_GPIO = GPIOD;
i2c.SDA_Pin = GPIO_PIN_3;
i2c.Delay_Time = 5; // 微秒
if(SoftI2C_Init(&i2c) != HAL_OK)
{
printf("SoftI2C Init Failed!\r\n");
while(1);
}
HAL_Delay(40); // 上电稳定
// 检测 SHT30
if(!SHT30_Check())
{
printf("SHT30 not detected!\r\n");
while(1);
}
while(1)
{
// 启动测量
SHT30_StartMeasure(&i2c);
HAL_Delay(15); // 高精度测量等待
// 读取数据
if(SHT30_Read(&i2c, &temperature, &humidity) == 0)
{
printf("Temperature: %.2f C, Humidity: %.2f %%\r\n", temperature, humidity);
}
else
{
printf("SHT30 Read Failed!\r\n");
}
HAL_Delay(1000); // 每 1 秒读取一次
}
}
void SystemClock_Config(void) {
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) {
Error_Handler();
}
PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_ADC;
PeriphClkInit.AdcClockSelection = RCC_ADCPCLK2_DIV6;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) {
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void) {
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1) {
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
然后将程序烧录到单片机中,通过串口助手既可以看到完整的SHT30数据。
