news 2026/8/28 14:58:16

24. 基于立创·地文星CW32F030C8T6的软件I2C驱动SHT20温湿度传感器实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
24. 基于立创·地文星CW32F030C8T6的软件I2C驱动SHT20温湿度传感器实战

24. 基于立创·地文星CW32F030C8T6的软件I2C驱动SHT20温湿度传感器实战

最近在做一个环境监测的小项目,需要用到温湿度传感器。手头正好有立创·地文星CW32F030C8T6开发板和SHT20传感器,但发现开发板的硬件I2C引脚已经被其他功能占用了。这时候,软件模拟I2C(也叫软件I2C)就成了一个非常灵活的解决方案。今天我就来手把手教大家,如何用CW32F030C8T6的任意两个GPIO引脚,通过软件模拟I2C协议,成功驱动SHT20温湿度传感器。

这篇文章非常适合刚开始接触嵌入式开发,或者对I2C通信协议还不太熟悉的朋友。咱们不扯那些复杂的理论,直接从硬件接线开始,一步步写代码,直到在串口上看到正确的温湿度数据。整个过程我会把每个步骤的原理和注意事项都讲清楚,确保你看完就能自己动手做出来。

1. 准备工作:认识SHT20和软件I2C

1.1 SHT20温湿度传感器简介

SHT20是一款非常常用的数字式温湿度传感器。它内部采用电容式测量技术,精度和稳定性都很好,最关键的是它使用标准的I2C接口和我们单片机通信,非常方便。

它的主要参数如下:

  • 测量范围:温度 -40°C 到 +125°C,湿度 0% 到 100% RH
  • 接口:I2C数字接口
  • 应用:空气质量监测、气象站、恒温恒湿控制、仓储管理等

简单来说,它就是一个通过两根线(SDA数据线和SCL时钟线)就能读取数据的“小模块”。

1.2 什么是软件I2C?

咱们的开发板(CW32F030C8T6)本身是有硬件I2C外设的,但硬件I2C的引脚是固定的(比如PB6、PB7)。如果这些引脚被其他功能(比如串口、SPI)占用了,或者你想把I2C功能放到更顺手的位置布线,硬件I2C就用不了了。

软件I2C就是来解决这个问题的。它的核心思想是:不用单片机自带的I2C硬件模块,而是用程序控制两个普通的GPIO引脚,一个模拟时钟线(SCL)的高低电平变化,另一个模拟数据线(SDA)的收发。这样,任何两个能输入输出的GPIO引脚都能变成I2C接口,非常自由。

当然,软件I2C也有缺点:它需要CPU不断地执行指令来模拟时序,会占用一些CPU资源,并且通信速度一般比硬件I2C慢。但对于读取SHT20这种不需要高速通信的传感器来说,软件I2C是完全够用且非常实用的。

1.3 硬件连接

接线非常简单,只需要四根线:

立创·地文星开发板引脚SHT20传感器引脚说明
3.3VVCC电源正极
GNDGND电源地
PB0SCLI2C时钟线(我们软件模拟)
PB1SDAI2C数据线(我们软件模拟)

注意:SHT20是3.3V器件,一定要接3.3V,不要接5V,否则可能损坏传感器。另外,I2C总线上通常需要加上拉电阻(一般4.7kΩ或10kΩ),但很多开发板(包括地文星)和传感器模块已经内置了,如果你的模块没有,需要在SDA和SCL线上各接一个上拉电阻到3.3V。

2. 软件I2C驱动编写:从引脚配置到完整协议

软件I2C的代码编写可以分成三个清晰的步骤:配置引脚、模拟I2C基础时序、编写针对SHT20的通信流程。咱们一步一步来。

2.1 第一步:配置GPIO引脚

既然我们要用PB0和PB1来模拟I2C,首先得把它们配置好。在嵌入式开发里,操作任何一个引脚前,都要做三件事:打开时钟、设置模式、初始化。

为了方便管理和修改,我们先把引脚定义做成宏,放在一个头文件里。我一般会创建一个bsp_sht20.h文件(bsp是板级支持包的意思)。

/* bsp_sht20.h */ #ifndef __BSP_SHT20_H__ #define __BSP_SHT20_H__ #include "board.h" // 1. 时钟和端口宏定义 #define RCC_SHT20_ENABLE() __RCC_GPIOB_CLK_ENABLE() // 使能GPIOB的时钟 #define PORT_SHT20 CW_GPIOB // 使用GPIOB端口 #define GPIO_SCL GPIO_PIN_0 // SCL时钟线使用PB0 #define GPIO_SDA GPIO_PIN_1 // SDA数据线使用PB1 // 2. SDA引脚模式切换宏(关键!) // SDA线在通信过程中需要在“输出模式”(主机发送数据)和“输入模式”(主机接收数据)之间切换 // 上拉输入模式:用于读取从机(SHT20)发送过来的数据 #define SDA_IN() { GPIO_InitTypeDef GPIO_InitStruct; \ GPIO_InitStruct.Pins = GPIO_SDA; \ GPIO_InitStruct.Mode = GPIO_MODE_INPUT_PULLUP; \ GPIO_InitStruct.Speed = GPIO_SPEED_HIGH; \ GPIO_Init(PORT_SHT20, &GPIO_InitStruct); \ } // 开漏输出模式:用于主机(单片机)向SDA线输出数据 // 注意:I2C协议要求总线是“线与”逻辑,必须使用开漏输出模式,并依靠外部上拉电阻拉到高电平。 #define SDA_OUT() { GPIO_InitTypeDef GPIO_InitStruct; \ GPIO_InitStruct.Pins = GPIO_SDA; \ GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; \ GPIO_InitStruct.Speed = GPIO_SPEED_HIGH; \ GPIO_Init(PORT_SHT20, &GPIO_InitStruct); \ } // 3. 引脚电平控制与读取宏 #define SCL(BIT) GPIO_WritePin(PORT_SHT20, GPIO_SCL, BIT?GPIO_Pin_SET:GPIO_Pin_RESET) #define SDA(BIT) GPIO_WritePin(PORT_SHT20, GPIO_SDA, BIT?GPIO_Pin_SET:GPIO_Pin_RESET) #define SDA_GET() GPIO_ReadPin(PORT_SHT20, GPIO_SDA) // 函数声明 void SHT20_GPIO_INIT(void); float SHT20_Read(unsigned char regaddr); #endif

这里有几个关键点:

  1. 开漏输出 (Output Open-Drain):这是I2C总线标准要求的模式。简单理解,开漏输出只能主动把线拉低(输出0),而不能主动输出高电平(1)。高电平是靠连接在总线上的上拉电阻拉上去的。这样,当多个设备连在总线上时,任何一个设备输出低电平,整条线就是低电平,实现了“线与”功能,避免了冲突。
  2. SDA模式切换:SDA线既要发送数据也要接收数据,所以必须在输出和输入模式之间动态切换。我们用SDA_OUT()SDA_IN()这两个宏来快速切换。

接下来,在bsp_sht20.c文件里实现引脚的初始化函数:

/* bsp_sht20.c - 引脚初始化部分 */ void SHT20_GPIO_INIT(void) { GPIO_InitTypeDef GPIO_InitStruct; // 定义一个GPIO初始化结构体 RCC_SHT20_ENABLE(); // 第一步:打开GPIOB的时钟开关 // 初始化SCL和SDA都为开漏输出模式 // 起始状态下,SDA也设置为输出,因为起始信号需要主机控制SDA GPIO_InitStruct.Pins = GPIO_SCL | GPIO_SDA; // 要初始化的引脚:PB0和PB1 GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 模式:开漏输出 GPIO_InitStruct.Speed = GPIO_SPEED_HIGH; // 输出速度:高速 GPIO_Init(PORT_SHT20, &GPIO_InitStruct); // 调用初始化函数 // 初始化后,将两条线都置为高电平(总线空闲状态) SCL(1); SDA(1); }

这个函数在程序开始的时候调用一次就行了,它把PB0和PB1都设置成了开漏输出,并且让它们处于高电平(总线空闲)。

2.2 第二步:模拟I2C基础时序

I2C通信就像两个人打电话,需要一套固定的“暗号”来开始对话、发送每一位数据、结束对话。这些“暗号”就是时序。我们需要用代码模拟出以下6个基本时序函数:

  1. 起始信号 (Start):通知总线上的所有设备:“我要开始说话了”。
  2. 停止信号 (Stop):通知设备:“我说完了”。
  3. 发送一个字节 (Write Byte):把8位数据一位一位地发出去。
  4. 读取一个字节 (Read Byte):从SDA线上一一位一位地读入8位数据。
  5. 发送应答 (Send ACK):主机告诉从机:“我收到你的数据了”。
  6. 等待应答 (Wait ACK):主机发送完地址或数据后,等待从机回复:“我收到了”。

这些函数的实现是软件I2C的核心,一定要理解时序图。不过别担心,代码我已经帮你写好了,并且加了详细注释。

/* bsp_sht20.c - I2C基础时序函数 */ /* 起始信号:SCL高电平时,SDA产生一个下降沿 */ void IIC_Start(void) { SDA_OUT(); // 确保SDA为输出模式 SCL(1); // 确保SCL为高 SDA(1); // 确保SDA为高 delay_us(5); // 保持一段时间 SDA(0); // SDA拉低,产生下降沿 -> 起始信号 delay_us(5); SCL(0); // 随后拉低SCL,准备发送数据 delay_us(5); } /* 停止信号:SCL高电平时,SDA产生一个上升沿 */ void IIC_Stop(void) { SDA_OUT(); SCL(0); // 先确保SCL为低 SDA(0); // 确保SDA为低 delay_us(5); SCL(1); // SCL拉高 delay_us(5); SDA(1); // SDA拉高,产生上升沿 -> 停止信号 delay_us(5); } /* 发送一个字节 (8位数据) */ void IIC_Write(uint8_t data) { uint8_t i; SDA_OUT(); SCL(0); // 拉低时钟线,准备数据变化 for(i = 0; i < 8; i++) { // 先放置数据位:取出最高位(bit7),右移7位后,不是1就是0 SDA((data & 0x80) >> 7); delay_us(2); // 数据建立时间 data <<= 1; // 左移,为发送下一位做准备 delay_us(6); SCL(1); // 拉高SCL,从机在此时刻采样SDA数据 delay_us(4); SCL(0); // 拉低SCL,完成一位数据的传输 delay_us(4); } } /* 读取一个字节 */ uint8_t IIC_Read(void) { uint8_t i, receive = 0; SDA_IN(); // 切换SDA为输入模式,准备读取 for(i = 0; i < 8; i++) { SCL(0); delay_us(5); SCL(1); // 拉高SCL,主机准备读取数据 delay_us(5); receive <<= 1; // 左移,为接收新数据位腾出最低位 if(SDA_GET()) // 读取当前SDA电平 { receive |= 1; // 如果为高电平,最低位置1 } delay_us(5); } return receive; } /* 主机发送应答(ACK)或非应答(NACK) */ // ack=0: 发送应答(ACK),拉低SDA // ack=1: 发送非应答(NACK),保持SDA高 void IIC_Send_Ack(uint8_t ack) { SDA_OUT(); SCL(0); if(!ack) SDA(0); // 发送ACK else SDA(1); // 发送NACK delay_us(5); SCL(1); // 产生一个时钟脉冲,从机采样应答位 delay_us(5); SCL(0); SDA(1); // 释放SDA线 } /* 主机等待从机的应答 */ // 返回0: 从机应答了(ACK) // 返回1: 从机无应答(NACK),可能设备地址错误或设备不存在 uint8_t IIC_Wait_Ack(void) { uint8_t ack_flag = 10; // 超时计数 SDA_IN(); // SDA设为输入,检测从机拉低的ACK信号 SDA(1); // 主机先释放SDA线(靠上拉电阻为高) delay_us(5); SCL(1); // 拉高SCL,从机应在此期间拉低SDA delay_us(5); // 循环检测SDA是否被从机拉低,并加入超时机制 while((SDA_GET() == 1) && (ack_flag)) { ack_flag--; delay_us(5); } if(ack_flag <= 0) // 超时,未收到应答 { IIC_Stop(); // 发送停止信号,释放总线 return 1; // 返回1表示失败 } else // 收到应答 { SCL(0); // 拉低SCL,结束ACK周期 SDA_OUT(); // 切换回输出模式,为后续发送做准备 return 0; // 返回0表示成功 } }

时序调试心得:这里的delay_us延时时间很关键。太快了传感器可能反应不过来,太慢了通信效率低。SHT20的I2C标准模式支持100kHz,你可以根据这个频率来估算延时。如果通信不稳定,可以适当增大这里的延时时间试试。

2.3 第三步:编写SHT20专属通信函数

有了基础的I2C“语言”能力,现在我们要用这套“语言”和SHT20这个特定的“人”对话。对话前,需要知道它的“电话号码”(设备地址)和“沟通流程”(通信协议)。

SHT20的关键信息:

  • I2C设备地址0x80(写操作时),0x81(读操作时)。这是7位地址0b1000000加上读写位构成的。
  • 工作模式:我们使用非主机模式(Non-Hold Master)。在这个模式下,传感器测量数据时,I2C总线(SCL线)不会被它独占,我们单片机还可以干别的事,更灵活。
  • 测量命令
    • 非主机模式读取温度:0xF3
    • 非主机模式读取湿度:0xF5

通信流程(以读取温度为例):整个流程就像我们打电话查天气:

  1. 拨号(起始信号+发送地址):发送起始信号,然后发送SHT20的写地址0x80,告诉它“我要跟你说话”。
  2. 说出指令(发送命令):发送温度测量命令0xF3,告诉它“请测量温度”。
  3. 等待并再次询问(等待+发起读请求):等待一小会儿让传感器测量,然后再次发送起始信号和SHT20的读地址0x81,问它“数据好了吗?好了就发给我”。
  4. 接收数据(读取3个字节):如果传感器回复“好了”(ACK),我们就连续读取3个字节。前两个字节是温度数据,第三个字节是CRC校验(本示例为简化未使用)。
  5. 挂电话(停止信号):数据读完后,发送停止信号。

根据这个流程,我们封装一个通用的读取函数SHT20_Read

/* bsp_sht20.c - 读取SHT20温湿度数据 */ float SHT20_Read(uint8_t regaddr) { unsigned char data_H = 0; // 数据高8位 unsigned char data_L = 0; // 数据低8位 float result = 0; // 换算后的最终值 unsigned short raw_data = 0; // 合并后的16位原始数据 // 1. 起始信号 + 发送设备写地址(0x80) IIC_Start(); IIC_Write(0x80); // 0x80 = 器件地址(0b1000000) + 写位(0) if(IIC_Wait_Ack() == 1) // 等待SHT20应答 { // 如果没应答,可能是接线问题或设备地址错误 printf("Error: No ACK after address write\r\n"); return -999.0; // 返回一个错误值 } // 2. 发送测量命令 (0xF3读温度,0xF5读湿度) IIC_Write(regaddr); if(IIC_Wait_Ack() == 1) { printf("Error: No ACK after command send\r\n"); return -999.0; } // 3. 等待测量完成,并发送读请求 // 在非主机模式下,需要不断询问传感器是否准备好数据 do { delay_us(10); // 等待一小段时间 IIC_Start(); // 重新发起起始信号 IIC_Write(0x81); // 发送设备读地址(0x81) // 如果传感器数据未准备好,会回复NACK,循环会继续 } while(IIC_Wait_Ack() == 1); // 等待ACK,收到ACK则跳出循环 delay_us(20); // 少量延时,确保数据稳定 // 4. 读取数据(共3字节:高8位、低8位、CRC8) data_H = IIC_Read(); // 读第一个字节(数据高8位) IIC_Send_Ack(0); // 主机回复ACK,要求继续发送 data_L = IIC_Read(); // 读第二个字节(数据低8位) IIC_Send_Ack(1); // 主机回复NACK(非应答),因为后面是CRC,我们这里不校验 // check = IIC_Read(); // 第三个字节是CRC校验,本示例跳过 IIC_Stop(); // 发送停止信号,结束通信 // 5. 数据处理 // 将两个8位数据合并成一个16位数据 raw_data = (data_H << 8) | data_L; // 清除低2位的状态位(根据SHT20手册,低2位是状态信息,不是测量数据) raw_data &= ~(0x0003); // 6. 数据换算(根据SHT20数据手册公式) if(regaddr == 0xf3) // 温度换算公式 { // 公式: T = -46.85 + 175.72 * (S / 2^16) result = (raw_data / 65536.0) * 175.72 - 46.85; } if(regaddr == 0xf5) // 湿度换算公式 { // 公式: RH = -6 + 125 * (S / 2^16) result = (raw_data / 65536.0) * 125.0 - 6.0; } return result; }

3. 实战验证:在主函数中调用

所有的底层驱动都写好了,现在到了最激动人心的环节:把它们用起来,看看能不能读到真实的温湿度。

我们在main.c文件中编写主程序:

/* main.c */ #include "board.h" #include "stdio.h" #include "bsp_uart.h" // 用于串口打印 #include "bsp_sht20.h" // 包含我们刚写的SHT20驱动 int32_t main(void) { board_init(); // 开发板基础初始化(系统时钟等) uart1_init(115200); // 初始化串口1,波特率115200,用于打印数据 // 初始化我们用于软件I2C的GPIO引脚(PB0, PB1) SHT20_GPIO_INIT(); // SHT20上电后需要一点时间初始化(手册要求至少15ms) delay_ms(20); printf("立创·地文星 SHT20 温湿度测试开始...\r\n"); while(1) { // 读取并打印温度值 float temperature = SHT20_Read(0xf3); printf("温度 = %.2f °C\r\n", temperature); // 读取并打印湿度值 float humidity = SHT20_Read(0xf5); printf("湿度 = %.2f %%RH\r\n", humidity); printf("-----------------\r\n"); delay_ms(1000); // 每隔1秒读取一次 } }

编译、下载到开发板,打开串口助手(波特率115200),你应该能看到类似下面的输出:

立创·地文星 SHT20 温湿度测试开始... 温度 = 25.36 °C 湿度 = 45.20 %RH ----------------- 温度 = 25.38 °C 湿度 = 45.18 %RH -----------------

看到这个,恭喜你!你已经成功用软件I2C驱动了SHT20传感器。

4. 常见问题与调试技巧

第一次做很可能不成功,别着急,嵌入式开发就是不断调试的过程。这里分享几个我踩过的坑:

  1. 没有任何输出,或者全是错误信息

    • 检查接线:这是最容易出错的地方!确保VCC、GND、SDA、SCL四根线都接对了,并且接触良好。
    • 检查电源:确认SHT20接的是3.3V,不是5V。
    • 检查上拉电阻:如果传感器模块上没有集成上拉电阻,一定要在SDA和SCL线上各接一个4.7kΩ的电阻到3.3V。
  2. 能收到数据,但数值明显不对(比如温度是-40或125)

    • 时序问题:软件I2C对延时非常敏感。尝试调整IIC_WriteIIC_Read等函数中的delay_us时间。可以稍微调大一点,比如都改成delay_us(10)
    • 数据位顺序:确认发送和接收数据时,是从最高位(MSB)开始还是最低位(LSB)开始。我们的代码是按照I2C标准,先发送最高位。
  3. 只能读到一次数据,后面就卡住了

    • 停止信号:确保每次完整的通信(无论是成功还是失败)后,都执行了IIC_Stop()来释放总线。
    • 从机无应答处理:检查IIC_Wait_Ack函数中的超时机制和错误处理。在等待ACK的循环里,如果超时一定要发送Stop信号并退出,否则总线会一直处于占用状态。

这个项目虽然小,但涵盖了嵌入式开发中非常核心的“软件模拟通信协议”的思想。掌握了这个方法,以后遇到SPI、单总线(如DHT11、DS18B20)等接口,你都可以用同样的思路去实现,再也不怕硬件引脚冲突了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/14 17:08:09

基于灵毓秀-牧神-造相Z-Turbo的小说解析器开发指南

基于灵毓秀-牧神-造相Z-Turbo的小说解析器开发指南 1. 引言 小说解析一直是文学研究和数字出版领域的重要需求。传统的人工解析方式耗时耗力&#xff0c;而且容易受到主观因素的影响。现在&#xff0c;借助灵毓秀-牧神-造相Z-Turbo模型&#xff0c;我们可以开发出智能的小说解…

作者头像 李华
网站建设 2026/7/14 17:08:08

Qwen3.5-27B开源模型部署案例:零下载权重、自动恢复服务的镜像优势

Qwen3.5-27B开源模型部署案例&#xff1a;零下载权重、自动恢复服务的镜像优势 1. 引言&#xff1a;告别繁琐部署&#xff0c;体验开箱即用的AI对话 如果你曾经尝试部署一个大型语言模型&#xff0c;大概率经历过这样的痛苦&#xff1a;先花几个小时甚至几天下载几十GB的模型…

作者头像 李华
网站建设 2026/7/14 17:08:10

MogFace-large企业级部署:高并发网络服务架构设计

MogFace-large企业级部署&#xff1a;高并发网络服务架构设计 最近和几个做安防和社交应用的朋友聊天&#xff0c;大家不约而同地提到了同一个痛点&#xff1a;人脸检测服务一到业务高峰期就扛不住。要么响应慢得像蜗牛&#xff0c;要么直接宕机&#xff0c;用户体验一落千丈。…

作者头像 李华
网站建设 2026/7/14 17:08:09

nomic-embed-text-v2-moe从零开始:Gradio前端交互+相似度验证全流程解析

nomic-embed-text-v2-moe从零开始&#xff1a;Gradio前端交互相似度验证全流程解析 1. 环境准备与快速部署 想要使用nomic-embed-text-v2-moe模型&#xff0c;首先需要完成环境部署。这个模型是一个强大的多语言文本嵌入模型&#xff0c;支持约100种语言&#xff0c;特别擅长…

作者头像 李华
网站建设 2026/7/14 17:08:10

嵌入式视觉伺服系统:基于仿射变换的双闭环激光追踪设计

1. 项目概述2023年全国大学生电子设计竞赛E题“运动目标控制与自动追踪系统”是一套高精度、强实时性的双闭环机电控制系统。该系统需在1米距离的白色屏幕上&#xff0c;以≤1cm光斑直径实现红色激光点的可控轨迹运动&#xff0c;并同步驱动绿色激光点完成对红点的亚厘米级动态…

作者头像 李华
网站建设 2026/7/14 17:08:07

如何突破教育资源获取壁垒?教育资源解析技术全攻略

如何突破教育资源获取壁垒&#xff1f;教育资源解析技术全攻略 【免费下载链接】tchMaterial-parser 国家中小学智慧教育平台 电子课本下载工具 项目地址: https://gitcode.com/GitHub_Trending/tc/tchMaterial-parser 在数字化教育普及的今天&#xff0c;教育资源的获取…

作者头像 李华