从竞赛题到实战项目:手把手教你用STM32和超声波模块DIY一个智能测距仪(附完整代码)
从竞赛题到实战项目:手把手教你用STM32和超声波模块DIY一个智能测距仪(附完整代码)
在电子设计竞赛中,我们常常会遇到各种功能模块的编程题目,比如超声波测距、LCD显示、按键控制等。这些题目虽然能考察选手的基础能力,但往往缺乏实际应用场景的连贯性。本文将带你把这些零散的技术点整合起来,打造一个真正实用的智能测距仪。
这个项目特别适合刚接触嵌入式开发的初学者,或者对电子DIY感兴趣的爱好者。我们将使用STM32F103C8T6(俗称"蓝莓派")作为主控,搭配常见的HC-SR04超声波模块和12864液晶屏,构建一个功能完善的测距设备。不同于简单的实验Demo,这个项目会教你如何设计完整的代码架构,处理实际应用中的各种问题。
1. 项目规划与硬件选型
1.1 核心功能设计
一个实用的测距仪需要具备以下基本功能:
- 实时距离测量与显示
- 历史数据记录(最远/最近值)
- 用户交互界面
- 数据校准功能
在此基础上,我们可以考虑添加一些扩展功能:
- 通过蓝牙模块上传数据到手机
- 设置报警阈值
- 数据记录与导出
1.2 硬件清单与连接
主控芯片:STM32F103C8T6开发板(性价比高,资源丰富)测距模块:HC-SR04超声波传感器(测量范围2cm-400cm)显示模块:12864液晶屏(带中文字库)其他组件:
- 按键 x4(功能控制)
- LED指示灯 x2(状态显示)
- 蜂鸣器(报警提示)
- HC-05蓝牙模块(可选)
接线示意图:
| 模块 | STM32引脚 | 说明 |
|---|---|---|
| HC-SR04 Trig | PB9 | 触发信号输出 |
| HC-SR04 Echo | PF8 | 回波信号输入 |
| 12864 SCL | PB6 | I2C时钟线 |
| 12864 SDA | PB7 | I2C数据线 |
| 按键1 | PA0 | 模式切换 |
| 按键2 | PA1 | 数据记录 |
2. 超声波测距模块的实现
2.1 工作原理与驱动代码
HC-SR04模块通过发送40kHz的超声波脉冲并接收回波来测量距离。计算公式为: 距离(cm) = (回波高电平时间 × 声速340m/s) / 2
以下是关键的初始化代码:
// 超声波模块初始化 void Ultrasonic_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; // 使能GPIO和AFIO时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOF|RCC_APB2Periph_AFIO, ENABLE); // 配置Trig引脚为推挽输出 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStruct); // 配置Echo引脚为浮空输入 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_8; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOF, &GPIO_InitStruct); // 配置外部中断 GPIO_EXTILineConfig(GPIO_PortSourceGPIOF, GPIO_PinSource8); EXTI_InitTypeDef EXTI_InitStruct; EXTI_InitStruct.EXTI_Line = EXTI_Line8; EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising_Falling; EXTI_InitStruct.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStruct); // 配置NVIC NVIC_InitTypeDef NVIC_InitStruct; NVIC_InitStruct.NVIC_IRQChannel = EXTI9_5_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStruct); }2.2 测量流程优化
在实际应用中,我们需要考虑以下问题:
- 多次测量取平均值提高精度
- 添加超时处理防止卡死
- 温度补偿(声速随温度变化)
改进后的测量函数:
float Get_Distance(void) { float sum = 0; uint8_t valid_count = 0; for(int i=0; i<5; i++) { // 发送触发信号 GPIO_SetBits(GPIOB, GPIO_Pin_9); delay_us(20); GPIO_ResetBits(GPIOB, GPIO_Pin_9); // 等待回波信号 uint32_t timeout = 0; while(GPIO_ReadInputDataBit(GPIOF, GPIO_Pin_8)==0) { if(++timeout > 10000) return -1; // 超时返回错误 delay_us(1); } // 测量高电平时间 uint32_t start = TIM2->CNT; while(GPIO_ReadInputDataBit(GPIOF, GPIO_Pin_8)); uint32_t duration = TIM2->CNT - start; float distance = duration * 0.017; // 340m/s ÷ 2 ÷ 10000 if(distance > 2 && distance < 400) { // 有效范围判断 sum += distance; valid_count++; } delay_ms(50); // 两次测量间隔 } return valid_count>0 ? sum/valid_count : -1; }提示:实际环境中超声波可能受到多种干扰,建议在代码中添加滤波算法,如中值滤波或卡尔曼滤波。
3. 用户界面设计与实现
3.1 12864液晶屏驱动
12864液晶屏可以通过I2C或并口驱动。这里我们使用常见的I2C方式,接线更简单:
// 初始化I2C void I2C_Config(void) { GPIO_InitTypeDef GPIO_InitStruct; I2C_InitTypeDef I2C_InitStruct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); // 配置PB6(SCL), PB7(SDA) GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStruct); I2C_InitStruct.I2C_Mode = I2C_Mode_I2C; I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2; I2C_InitStruct.I2C_OwnAddress1 = 0x00; I2C_InitStruct.I2C_Ack = I2C_Ack_Enable; I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_InitStruct.I2C_ClockSpeed = 100000; // 100kHz I2C_Init(I2C1, &I2C_InitStruct); I2C_Cmd(I2C1, ENABLE); }3.2 界面布局与状态管理
一个良好的用户界面应该包含:
- 实时测量数据显示区
- 历史记录查看区
- 系统状态指示区
- 操作提示区
我们使用状态机模式来管理界面:
typedef enum { MODE_MEASURE, // 测量模式 MODE_HISTORY, // 历史记录查看 MODE_SETTING, // 系统设置 MODE_CALIBRATE // 校准模式 } SystemMode; void Update_Display(SystemMode mode) { LCD_Clear(); switch(mode) { case MODE_MEASURE: LCD_ShowString(0, 0, "当前距离:"); LCD_ShowFloat(60, 0, current_distance, 1); LCD_ShowString(120, 0, "cm"); LCD_ShowString(0, 2, "最大值:"); LCD_ShowFloat(60, 2, max_distance, 1); LCD_ShowString(0, 3, "最小值:"); LCD_ShowFloat(60, 3, min_distance, 1); break; case MODE_HISTORY: // 历史记录显示逻辑 break; // 其他模式显示... } // 显示公共元素 LCD_ShowString(90, 3, "BAT:80%"); }4. 数据存储与蓝牙传输
4.1 EEPROM数据存储
为了防止断电数据丢失,我们需要将关键数据保存到EEPROM中。STM32内部没有真正的EEPROM,但可以用Flash模拟:
#define EEPROM_START_ADDR 0x0800F000 void EE_WriteFloat(uint32_t addr, float data) { uint32_t data_tmp = *(uint32_t*)&data; FLASH_Unlock(); FLASH_ErasePage(EEPROM_START_ADDR); FLASH_ProgramWord(EEPROM_START_ADDR + addr, data_tmp); FLASH_Lock(); } float EE_ReadFloat(uint32_t addr) { uint32_t data_tmp = *(uint32_t*)(EEPROM_START_ADDR + addr); return *(float*)&data_tmp; }4.2 蓝牙模块集成
HC-05蓝牙模块可以通过串口与STM32通信,实现数据无线传输:
void USART1_Init(uint32_t baudrate) { GPIO_InitTypeDef GPIO_InitStruct; USART_InitTypeDef USART_InitStruct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); // 配置PA9(TX), PA10(RX) GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStruct); USART_InitStruct.USART_BaudRate = baudrate; USART_InitStruct.USART_WordLength = USART_WordLength_8b; USART_InitStruct.USART_StopBits = USART_StopBits_1; USART_InitStruct.USART_Parity = USART_Parity_No; USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; USART_Init(USART1, &USART_InitStruct); USART_Cmd(USART1, ENABLE); } void Bluetooth_SendData(float distance) { char buffer[32]; sprintf(buffer, "DIST:%.1fcm\n", distance); for(int i=0; buffer[i]!='\0'; i++) { USART_SendData(USART1, buffer[i]); while(USART_GetFlagStatus(USART1, USART_FLAG_TXE)==RESET); } }5. 系统整合与调试技巧
5.1 主程序架构
一个好的嵌入式程序应该采用模块化设计,主循环保持简洁:
int main(void) { // 硬件初始化 System_Init(); Ultrasonic_Init(); LCD_Init(); Key_Init(); USART1_Init(9600); // 从EEPROM加载历史数据 max_distance = EE_ReadFloat(0); min_distance = EE_ReadFloat(4); while(1) { // 1. 读取按键 Key_Scan(); // 2. 测量距离 current_distance = Get_Distance(); // 3. 更新历史记录 if(current_distance > max_distance) { max_distance = current_distance; EE_WriteFloat(0, max_distance); } if(current_distance < min_distance) { min_distance = current_distance; EE_WriteFloat(4, min_distance); } // 4. 更新显示 Update_Display(current_mode); // 5. 蓝牙传输 if(bluetooth_enable) { Bluetooth_SendData(current_distance); } delay_ms(100); } }5.2 常见问题排查
在实际制作过程中,你可能会遇到以下问题:
超声波模块无响应
- 检查Trig和Echo接线是否正确
- 确保供电电压在5V左右
- 测量Trig信号是否正常发出(可用示波器观察)
LCD显示异常
- 确认I2C地址是否正确(通常0x3F或0x27)
- 检查对比度调节电位器
- 确保初始化序列完整
蓝牙连接不稳定
- 检查波特率设置是否匹配
- 确保模块进入AT模式时波特率为38400
- 避免强电磁干扰环境
注意:调试时建议先单独测试每个模块,确认正常工作后再进行系统集成。使用逻辑分析仪或示波器可以大大简化调试过程。
