STM32F103C8T6标准库项目实战:从零DIY一个温湿度监测器(OLED显示+ESP8266上传)
STM32F103C8T6标准库项目实战从零DIY一个温湿度监测器OLED显示ESP8266上传1. 项目规划与硬件选型在嵌入式开发中一个完整的物联网项目通常包含传感器数据采集、本地显示和远程传输三个核心环节。选择STM32F103C8T6作为主控芯片不仅因为其性价比高更因其丰富的外设资源足以应对这类项目的需求。硬件清单主控STM32F103C8T6最小系统板蓝色药丸温湿度传感器DHT11或DHT22根据精度需求选择显示模块0.96寸OLEDSSD1306驱动I2C接口无线模块ESP8266-01AT指令固件其他杜邦线、面包板、USB转TTL模块注意DHT22比DHT11精度更高但价格稍贵ESP8266建议选择已烧录AT固件的版本以简化开发。硬件连接示意图模块STM32引脚连接方式OLED SCLPB6I2C1_SCLOLED SDAPB7I2C1_SDADHT11 DATAPA0GPIO输入ESP8266 TXPA9USART1_TXESP8266 RXPA10USART1_RX2. 开发环境搭建与工程配置虽然许多教程会花大量篇幅讲解环境搭建但这里我们更关注实际项目开发。假设你已经完成以下基础准备Keil MDK 5已安装并激活STM32F10x标准库已下载ST-Link调试器驱动正常工程配置关键步骤新建Keil工程选择STM32F103C8T6器件添加标准库必要文件core_cm3.cstartup_stm32f10x_md.sstm32f10x_rcc.cstm32f10x_gpio.cstm32f10x_i2c.cstm32f10x_usart.c配置编译器选项// 在Options for Target → C/C 中添加宏定义 USE_STDPERIPH_DRIVER, STM32F10X_MD设置调试工具为ST-Link并勾选Reset and Run3. 外设驱动开发3.1 OLED显示驱动SSD1306 OLED屏通过I2C接口通信首先需要初始化I2C外设void I2C_Configuration(void) { GPIO_InitTypeDef GPIO_InitStructure; I2C_InitTypeDef I2C_InitStructure; // 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); // 配置GPIO GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_OD; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // 配置I2C I2C_InitStructure.I2C_Mode I2C_Mode_I2C; I2C_InitStructure.I2C_DutyCycle I2C_DutyCycle_2; I2C_InitStructure.I2C_OwnAddress1 0x00; I2C_InitStructure.I2C_Ack I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress I2C_AcknowledgedAddress_7bit; I2C_InitStructure.I2C_ClockSpeed 400000; // 400kHz I2C_Init(I2C1, I2C_InitStructure); I2C_Cmd(I2C1, ENABLE); }OLED显示函数示例显示字符串void OLED_ShowString(uint8_t x, uint8_t y, char *str) { while(*str ! \0) { OLED_ShowChar(x, y, *str); x 8; if(x 120) { x 0; y 2; } str; } }3.2 DHT11温湿度传感器驱动DHT11采用单总线协议需要精确的时序控制#define DHT11_GPIO_PORT GPIOA #define DHT11_GPIO_PIN GPIO_Pin_0 uint8_t DHT11_Read_Data(uint8_t *temp, uint8_t *humi) { uint8_t buf[5] {0}; uint8_t i, j; // 主机拉低至少18ms GPIO_ResetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN); Delay_ms(20); GPIO_SetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN); // 等待传感器响应 while(GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN)); while(!GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN)); while(GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN)); // 读取40位数据 for(i0; i5; i) { for(j0; j8; j) { while(!GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN)); Delay_us(30); if(GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN)) { buf[i] | (1 (7-j)); while(GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN)); } } } // 校验数据 if(buf[0] buf[1] buf[2] buf[3] buf[4]) { *humi buf[0]; *temp buf[2]; return 1; } return 0; }3.3 ESP8266 WiFi模块通信ESP8266通过AT指令与STM32通信需要配置USARTvoid USART1_Init(uint32_t baudrate) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); // 配置TX(PA9)为复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 配置RX(PA10)为浮空输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate baudrate; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); USART_Cmd(USART1, ENABLE); }发送AT指令并等待响应的函数uint8_t ESP8266_SendCmd(char *cmd, char *ack, uint16_t waittime) { char resp[100] {0}; uint16_t i 0; USART_SendString(USART1, cmd); Delay_ms(100); while(waittime--) { if(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) ! RESET) { resp[i] USART_ReceiveData(USART1); if(i sizeof(resp)-1) break; } Delay_ms(1); } if(strstr(resp, ack) ! NULL) { return 1; } return 0; }4. 系统整合与数据上传4.1 主程序逻辑设计系统主循环应该包含以下功能定时读取温湿度数据更新OLED显示检查是否需要上传数据处理用户输入如果有int main(void) { uint8_t temp, humi; uint32_t last_read 0, last_upload 0; SystemInit(); Delay_Init(); I2C_Configuration(); OLED_Init(); USART1_Init(115200); DHT11_Init(); // 初始化ESP8266 ESP8266_SendCmd(ATRST\r\n, ready, 2000); ESP8266_SendCmd(ATCWMODE1\r\n, OK, 1000); ESP8266_SendCmd(ATCWJAP\SSID\,\password\\r\n, OK, 5000); while(1) { // 每2秒读取一次温湿度 if(HAL_GetTick() - last_read 2000) { if(DHT11_Read_Data(temp, humi)) { OLED_Clear(); OLED_ShowString(0, 0, Temp:); OLED_ShowNum(40, 0, temp, 2); OLED_ShowString(0, 2, Humi:); OLED_ShowNum(40, 2, humi, 2); last_read HAL_GetTick(); } } // 每10秒上传一次数据 if(HAL_GetTick() - last_upload 10000) { char buf[50]; sprintf(buf, ATCIPSTART\TCP\,\api.thingspeak.com\,80\r\n); ESP8266_SendCmd(buf, CONNECT, 2000); sprintf(buf, GET /update?api_keyYOUR_KEYfield1%dfield2%d\r\n, temp, humi); ESP8266_SendCmd(ATCIPSEND50\r\n, , 1000); ESP8266_SendCmd(buf, SEND OK, 2000); ESP8266_SendCmd(ATCIPCLOSE\r\n, CLOSED, 1000); last_upload HAL_GetTick(); } } }4.2 数据上传方案选择根据项目需求可以选择不同的数据上传方式Thingspeak平台免费账户支持每15秒更新一次数据提供基本的数据可视化功能适合快速原型开发自建TCP服务器需要一台有公网IP的服务器使用Python等语言编写简单的TCP服务数据存储更灵活MQTT协议使用公共MQTT broker如EMQX Cloud低功耗适合电池供电设备需要额外的协议实现提示对于初学者建议先从Thingspeak开始熟悉基本流程后再尝试其他方案。5. 项目优化与扩展5.1 低功耗优化如果项目需要电池供电可以考虑以下优化措施使用STM32的低功耗模式Stop模式减少数据上传频率关闭不必要的外设时钟使用硬件定时器唤醒void Enter_Stop_Mode(uint32_t wakeup_time) { // 配置唤醒引脚 EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPD; GPIO_Init(GPIOA, GPIO_InitStructure); EXTI_InitStructure.EXTI_Line EXTI_Line0; EXTI_InitStructure.EXTI_Mode EXTI_Mode_Event; EXTI_InitStructure.EXTI_Trigger EXTI_Trigger_Rising; EXTI_InitStructure.EXTI_LineCmd ENABLE; EXTI_Init(EXTI_InitStructure); // 进入Stop模式 PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI); // 唤醒后重新配置时钟 SystemInit(); Delay_Init(); }5.2 扩展功能基于现有硬件可以轻松扩展更多功能添加按键控制切换显示模式集成光敏传感器自动调节OLED亮度增加蜂鸣器温湿度超限报警使用RTC芯片添加时间戳// 按键检测示例 uint8_t Key_Scan(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) { if(GPIO_ReadInputDataBit(GPIOx, GPIO_Pin) 0) { Delay_ms(10); if(GPIO_ReadInputDataBit(GPIOx, GPIO_Pin) 0) { while(GPIO_ReadInputDataBit(GPIOx, GPIO_Pin) 0); return 1; } } return 0; }6. 常见问题与调试技巧6.1 硬件连接问题症状OLED不显示或显示乱码检查I2C地址是否正确通常0x78或0x7A确认上拉电阻是否接好4.7kΩ测量SCL/SDA信号是否正常症状DHT11读取失败检查DATA线是否接好确保供电电压足够3.3V可能不稳定建议5V调整时序延迟不同批次传感器可能有差异6.2 软件调试技巧串口打印调试信息printf(DHT11 Read: temp%d, humi%d\r\n, temp, humi);使用逻辑分析仪抓取I2C通信波形分析DHT11时序查看ESP8266 AT指令交互分段测试先单独测试OLED显示再单独测试DHT11读取最后整合WiFi功能6.3 性能优化建议减少全局变量使用局部变量提高效率合理使用中断关键操作使用中断而非轮询代码模块化将功能分解为独立的.c/.h文件版本控制使用Git管理代码变更// 示例使用DMA提高USART传输效率 void USART1_DMA_Init(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel4); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)sendBuffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize 0; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Normal; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel4, DMA_InitStructure); USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); }在实际项目中我发现最常遇到的问题往往是电源不稳定导致的传感器读数异常。建议在DHT11的VCC和GND之间加一个100nF的电容能显著提高读取稳定性。另外ESP8266模块对电源质量要求较高最好单独供电或使用大容量滤波电容。