在很多单片机课设、毕设和电子竞赛题目中,“基于STM32单片机电机状态检测”是一个很经典的方向。起初很多同学会把它简单理解成“用霍尔传感器测转速,然后显示在屏幕上”,但实际做一个完整项目时你会发现,电机堵转、过流、过热、停转这些异常状态,往往比单纯的转速值更有价值。本文就以这套题目为主线,从硬件选型、检测原理、代码实现到调试排错,整理出一套可以直接参考的闭环方案。
阅读本文后,你可以掌握电机电流的ADC采样与换算、测速脉冲的输入捕获测量、DS18B20温度读取、状态判断与报警逻辑,以及串口调试方法。代码基于STM32F103C8T6和标准外设库编写,适合项目实战和二次扩展。
1. 项目背景:为什么需要电机状态检测
1.1 电机状态检测解决什么问题
电机在运行时,不是只有“转”和“不转”两种状态。实际工程中常见的异常情况包括:
- 电机启动后堵转,电流迅速升高,若不及时处理会导致绕组过热烧毁。
- 负载过大导致电机过流,转速下降但电机仍在运行,时间久了会损坏机械结构。
- 电机线圈或驱动电路异常导致温度升高,表面可能看不出来。
- 电机因供电异常、驱动故障而停止转动,但上位机可能仍然认为它处于工作状态。
电机状态检测系统的作用,就是通过传感器采集电机的电流、转速、温度等物理量,并把它们转换为可处理的电信号,再由单片机进行判断和报警。这样,系统不仅能知道“电机在不在转”,还能知道“电机是否处于健康状态”。
1.2 需要检测哪些核心状态
从项目可实现性和工程实用角度来看,无需一开始就做振动分析、噪声分析等复杂手段,建议从这三个物理量入手:
| 检测量 | 选型方案 | 能反映什么问题 |
|---|---|---|
| 电流 | ACS712 霍尔电流传感器模块 | 过流、堵转、负载异常 |
| 转速 | 霍尔测速模块或光电编码器 | 停转、超速、转速异常 |
| 温度 | DS18B20 数字温度传感器 | 过热、散热不良 |
这三个量互相结合后,就可以形成比较完整的电机健康状态判断逻辑:
- 转速为 0 且电流接近 0,说明电机处于停止状态。
- 转速为 0 但电流很大,说明电机可能堵转,需要立即保护。
- 电流超过额定值但转速正常,说明负载偏大,需要观察。
- 温度持续上升并超过阈值,说明散热异常或连续过载。
1.3 系统整体方案
本文采用 STM32F103C8T6 作为主控,整体工作流程如下:
- 电流传感器输出模拟电压,接入 STM32 的 ADC 引脚。
- 测速模块输出脉冲信号,接入 STM32 定时器输入捕获引脚。
- DS18B20 通过单总线协议把温度数据传给 STM32。
- 单片机周期性采集数据,经过软件滤波和公式换算得到真实物理量。
- 程序根据设定阈值判断当前电机状态。
- 串口输出调试数据,蜂鸣器在异常状态时报警。
这个方案成本低、原理清晰、代码可读性强,后续如果要扩展 PID 调速、WiFi 监控、触摸屏显示,也都有足够的升级空间。
2. 硬件选型与系统框图
2.1 主控芯片选择
项目中常用的 STM32 芯片有 F103C8T6、F103ZET6、F407 等。对于电机状态检测这种中低速采集场景,STM32F103C8T6 已经完全够用:
- 72MHz 主频,满足 ADC 采样和输入捕获实时性要求。
- 3 个 USART,方便串口调试。
- 2 个高级定时器、4 个通用定时器,输入捕获测速很灵活。
- 10 个 ADC 通道,后续扩展多路电流检测也没有压力。
需要注意的是,如果使用 STM32F103C8T6 最小系统板,板载通常已经包含 8MHz 晶振和复位电路,使用时只需要接 ST-Link 下载器即可。
2.2 传感器与模块选型
电流检测模块选型时,要关注量程和灵敏度。ACS712 模块常见量程有 5A、20A、30A。本文示例使用 ACS712-05B,灵敏度约 185mV/A,供电电压 5V,零电流时输出约 2.5V。这个输出范围在 STM32 的 3.3V ADC 量程内,可以直接连接。
转速检测模块使用霍尔测速模块,常见方案有两种:
- 带霍尔传感器的测速盘模块,电机转动时输出方波脉冲。
- 光电编码器模块,通过码盘遮挡输出脉冲。
两者对 STM32 来说都是“脉冲输入”,处理方式一致。本文以霍尔测速模块作为示例,输出脉冲接入定时器输入捕获引脚。
温度检测推荐 DS18B20,单总线数字输出,不需要 ADC 通道,而且测量精度可以做到 0.5℃ 左右,非常适合电机外壳温度检测。
2.3 模块接线表
参考接线如下:
| 模块 | 模块引脚 | STM32 引脚 | 说明 |
|---|---|---|---|
| ACS712 电流模块 | VCC | 5V | 外部 5V 供电 |
| ACS712 电流模块 | OUT | PA0 | ADC1 通道 0 |
| ACS712 电流模块 | GND | GND | 与 STM32 共地 |
| 霍尔测速模块 | VCC | 3.3V 或 5V | 根据模块要求 |
| 霍尔测速模块 | OUT | PA6 | TIM3_CH1 输入捕获 |
| DS18B20 | DQ | PB0 | 单总线数据线,需接 4.7k 上拉 |
| 蜂鸣器模块 | I/O | PB1 | 低电平触发,根据模块调整 |
| 串口模块 | TX | PA10 | USART1_RX |
| 串口模块 | RX | PA9 | USART1_TX |
接线时一定要保证所有模块与 STM32 共地,否则传感器输出的信号电平没有参考基准,ADC 读数和脉冲判断都会异常。
3. 开发环境准备与工程搭建
3.1 开发工具说明
本文代码基于以下环境编写:
- 芯片:STM32F103C8T6
- IDE:Keil MDK5
- 标准外设库:STM32F10x 标准外设库 V3.5
- 下载调试器:ST-Link V2
- 串口调试助手:任意支持 ASCII/HEX 显示的软件即可
标准外设库虽然相比 HAL 库略显繁琐,但它的寄存器封装更接近硬件底层,适合学习 ADC、定时器、GPIO 这些核心外设的工作原理。如果项目时间紧,也可以改到 HAL 库,但底层的采集逻辑和换算公式是一样的。
3.2 新建标准库工程的要点
STM32 标准库工程的搭建通常包含以下部分:
- 启动文件
startup_stm32f10x_md.s。 - 标准外设库的
src和inc目录。 - CMSIS 核心文件
core_cm3.c、system_stm32f10x.c。 - 自己编写的
main.c、adc.c、timer.c、delay.c等应用文件。
如果使用 Keil MDK5,还可以利用 RTE 环境直接添加标准库组件。但无论使用哪种方式,都要注意芯片型号选择为 STM32F103C8,并在 C/C++ 选项中定义STM32F10X_MD宏。没有这个宏,标准外设库的很多配置结构体不会被正确编译。
3.3 工程目录规划
为了让后续维护方便,建议建立清晰的文件结构:
Project/ ├── User/ │ ├── main.c │ ├── adc.c │ ├── adc.h │ ├── timer.c │ ├── timer.h │ ├── ds18b20.c │ ├── ds18b20.h │ ├── delay.c │ └── delay.h ├── STM32F10x_FWLib/ │ ├── inc/ │ └── src/ ├── CMSIS/ │ ├── core_cm3.c │ ├── core_cm3.h │ ├── system_stm32f10x.c │ └── startup_stm32f10x_md.s └── Project.uvprojx4. 核心检测原理拆解
4.1 电流检测:ADC采样与换算公式
ACS712-05B 在无电流时输出电压约为 2.5V,灵敏度为 185mV/A。
STM32 的 ADC 是 12 位,参考电压为 3.3V,因此 ADC 值与电压的换算关系为:
电压值 = ADC值 / 4096.0 × 3.3V已知电压值后,再用以下公式反推电流:
电流值 = (电压值 - 2.5V) / 0.185V/A这里有两个容易踩坑的点:
- ACS712 的 2.5V 是典型中点电压,实际模块会有偏差,最好在无电流时实测校准。
- 如果电流方向接反,计算出的电流可能为负数。这并不一定代表故障,而是传感器输出反向偏移所致,可根据需求做绝对值处理。
为了提高采样稳定性,程序中可以对多次 ADC 采样结果取平均值,这样可以有效滤除部分随机噪声。
4.2 转速检测:输入捕获与周期换算
测速模块输出的是脉冲信号,电机每转一圈会产生一定数量的脉冲。假设每转脉冲数为 P,那么相邻两个上升沿之间的时间差就是一个脉冲周期 T。转速公式为:
转速 n(rpm)= 60 / (T × P)例如每转输出 1 个脉冲,如果相邻脉冲周期为 0.1 秒,那么转速就是 60 / 0.1 = 600rpm。
在 STM32 中,可以使用定时器输入捕获功能测量脉冲周期。输入捕获的原理是:定时器自由计数,当检测到引脚上升沿时,硬件自动把当前计数值保存到捕获寄存器。前后两次捕获寄存器值的差值,就是脉冲周期对应的计数值。
本文示例使用 TIM3_CH1,配置预分频为 720 分频。系统时钟为 72MHz 时,计数频率为 100kHz,每个计数单位代表 0.01ms。
这样一来,如果捕获差值为 period,脉冲周期为:
T = period × 0.01ms = period × 0.00001s代入转速公式后得到:
转速 n(rpm)= 6000000.0 / (period × P)4.3 温度检测:DS18B20单总线读取
DS18B20 使用单总线协议,数据线和时钟线复用为一根线。通信过程分成三个步骤:
- 复位脉冲:主机把总线拉低一段时间,然后释放,等待 DS18B20 应答。
- 写入 ROM 指令和功能指令,例如跳过 ROM(0xCC)、启动温度转换(0x44)、读取暂存器(0xBE)。
- 读取温度数据,DS18B20 返回两个字节,低字节和高字节,按 0.0625℃/LSB 换算。
DS18B20 驱动代码比较固定,核心读取函数可以写成下面这种形式:
float DS18B20_Get_Temperature(void) { uint8_t temp_l = 0; uint8_t temp_h = 0; int16_t temp = 0; DS18B20_Reset(); DS18B20_WriteByte(0xCC); // 跳过ROM DS18B20_WriteByte(0x44); // 启动温度转换 Delay_Us(750); DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); // 读取暂存器 temp_l = DS18B20_ReadByte(); temp_h = DS18B20_ReadByte(); temp = (temp_h << 8) | temp_l; return temp * 0.0625f; }需要注意的是,DS18B20 对时序要求比较严格。如果从网上移植驱动,一定要确认底层延时函数的精度,尤其是微秒级延时。很多“读出来永远是 85℃”的问题,本质上就是复位时序或者读写时序不满足要求。
4.4 状态判断策略
状态判断是整套软件的核心。本文把电机状态定义为一个枚举类型,方便后续扩展:
| 状态 | 判断条件 |
|---|---|
| 正常 | 转速大于停止阈值,电流小于过流阈值,温度正常 |
| 停止 | 转速低于停止阈值,电流接近 0 |
| 堵转 | 转速低于停止阈值,电流超过堵转阈值 |
| 过流 | 电流超过过流阈值,但转速仍存在 |
| 过热 | 温度超过温度报警阈值 |
| 超速 | 转速超过速度上限 |
判断时需要特别注意顺序。堵转通常同时满足“转速低”和“电流大”,如果先判断过流,可能把堵转误判为普通过流。因此建议先判断堵转,再判断过流。
5. 软件设计与完整代码实现
5.1 代码结构说明
代码采用模块化设计:
adc.c:负责 ADC 初始化和电流采样。timer.c:负责 TIM3 输入捕获初始化,在中断中计算脉冲周期。ds18b20.c:负责温度读取。main.c:负责系统初始化、数据汇总、状态判断、串口输出和报警控制。
下面给出每个模块的核心代码。示例中使用的是标准外设库 V3.5,如果你使用 HAL 库,配置思路仍然一致,只是 API 名称不同。
5.2 ADC电流采集模块
文件:adc.h
#ifndef __ADC_H #define __ADC_H #include "stm32f10x.h" void ADC_Config(void); float Read_Current_A(void); #endif文件:adc.c
#include "adc.h" #include "delay.h" #define ACS712_VREF 2.5f #define ACS712_SENSITIVITY 0.185f void ADC_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); } float Read_Current_A(void) { uint32_t sum = 0; float voltage; float current; uint8_t i; for (i = 0; i < 20; i++) { ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); sum += ADC_GetConversionValue(ADC1); } voltage = (sum / 20.0f) / 4096.0f * 3.3f; current = (voltage - ACS712_VREF) / ACS712_SENSITIVITY; return current; }代码中采样了 20 次 ADC 并求平均,目的是减少电压抖动。采样周期也可以根据实际情况调整,如果检测响应要求更快,可以减少采样次数。
5.3 定时器输入捕获测速模块
文件:timer.h
#ifndef __TIMER_H #define __TIMER_H #include "stm32f10x.h" void TIM3_Cap_Init(void); float Get_Speed_RPM(uint16_t pulses_per_rev); #endif文件:timer.c
#include "timer.h" volatile uint16_t cap_1 = 0; volatile uint16_t cap_2 = 0; volatile uint16_t cap_period = 0; void TIM3_Cap_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler = 720 - 1; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter = 0x0F; TIM_ICInit(TIM3, &TIM_ICInitStructure); NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); TIM_Cmd(TIM3, ENABLE); TIM_ClearFlag(TIM3, TIM_FLAG_CC1); } void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_CC1) != RESET) { cap_2 = cap_1; cap_1 = TIM_GetCapture1(TIM3); if (cap_1 > cap_2) { cap_period = cap_1 - cap_2; } TIM_ClearITPendingBit(TIM3, TIM_IT_CC1); } } float Get_Speed_RPM(uint16_t pulses_per_rev) { if (cap_period == 0) { return 0.0f; } return 6000000.0f / ((float)cap_period * pulses_per_rev); }这里采用测周法计算转速,即通过测量相邻脉冲的时间间隔来算出转速。测周法适合中高速测量,转速越低,单个脉冲周期越长,测量精度越高。如果电机转速非常高,脉冲频率很高,则更适合改用测频法,例如在固定时间窗口内统计脉冲个数。
5.4 主函数与状态判断逻辑
文件:main.c
#include "stm32f10x.h" #include "adc.h" #include "timer.h" #include "ds18b20.h" #include "delay.h" #include "stdio.h" #define PULSES_PER_REV 1 #define CURRENT_OVERLOAD_THRESH 1.5f #define CURRENT_BLOCK_THRESH 2.0f #define SPEED_STOP_THRESHOLD 5.0f #define TEMP_ALARM_THRESHOLD 60.0f typedef enum { STATE_NORMAL = 0, STATE_STOP, STATE_BLOCKED, STATE_OVER_CURRENT, STATE_OVER_TEMP, STATE_OVER_SPEED } MotorState; void USART1_Init(uint32_t baudrate) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); 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); 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); } int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, (uint8_t)ch); return ch; } void BEEP_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_SetBits(GPIOB, GPIO_Pin_1); } void BEEP_ON(void) { GPIO_ResetBits(GPIOB, GPIO_Pin_1); } void BEEP_OFF(void) { GPIO_SetBits(GPIOB, GPIO_Pin_1); } MotorState Judge_MotorState(float current, float speed, float temperature) { if (speed < SPEED_STOP_THRESHOLD && current > CURRENT_BLOCK_THRESHOLD) { return STATE_BLOCKED; } if (current > CURRENT_OVERLOAD_THRESHOLD) { return STATE_OVER_CURRENT; } if (temperature > TEMP_ALARM_THRESHOLD) { return STATE_OVER_TEMP; } if (speed < SPEED_STOP_THRESHOLD) { return STATE_STOP; } return STATE_NORMAL; } int main(void) { float current = 0.0f; float speed = 0.0f; float temperature = 0.0f; MotorState state = STATE_NORMAL; Delay_Init(); USART1_Init(115200); ADC_Config(); TIM3_Cap_Init(); DS18B20_Init(); BEEP_Init(); printf("Motor State Detect System Start\r\n"); while (1) { current = Read_Current_A(); speed = Get_Speed_RPM(PULSES_PER_REV); temperature = DS18B20_Get_Temperature(); state = Judge_MotorState(current, speed, temperature); printf("I=%.2fA S=%.1frpm T=%.1fC State=%d\r\n", current, speed, temperature, state); if (state == STATE_BLOCKED || state == STATE_OVER_TEMP) { BEEP_ON(); } else { BEEP_OFF(); } Delay_Ms(200); } }主函数中的printf重定向依赖fputc函数,使用串口助手可以在 PC 上直接看到采集数据。
如果项目要求使用 LCD1602 或 OLED 显示,只需要在状态判断完成之后增加一个Display_Update()接口,在接口内部根据state刷新屏幕内容即可,核心检测逻辑无需改动。
5.5 温度读取模块说明
由于 DS18B20 底层驱动代码量较大,且网上成熟代码很多,这里不重复粘贴完整文件。你需要在自己的工程中新建ds18b20.h和ds18b20.c,至少实现以下接口:
void DS18B20_Init(void); float DS18B20_Get_Temperature(void);需要注意以下几点:
- 数据线必须接上拉电阻,一般使用 4.7kΩ。
- 初始化时序的复位脉冲时间要符合手册要求,480us 以上。
- 读写位时序要求严格,建议使用微秒级延时函数。
- 如果读取结果一直为 85℃,说明复位或读取时序有问题,优先检查延时。
6. 运行调试与结果分析
6.1 串口观察数据
程序下载后,打开串口调试助手,波特率设置为 115200,正常情况下可以看到类似输出:
Motor State Detect System Start I=0.03A S=0.0rpm T=28.5C State=1 I=0.05A S=0.0rpm T=28.6C State=1此时电机没有启动,电流接近 0,转速为 0,状态为停止。
当电机启动后,输出可能变为:
I=0.82A S=320.5rpm T=31.2C State=0 I=0.81A S=321.0rpm T=31.4C State=0此时状态为正常。
6.2 人为模拟故障验证
调试阶段可以人为制造故障,验证判断逻辑是否正确:
- 用手轻触电机轴增加阻力,观察电流是否上升、转速是否下降。
- 堵住电机转轴,确认系统能在短时间内识别堵转并触发蜂鸣器。
- 加热 DS18B20 探头,验证过热报警功能。
需要注意的是,堵转测试时间不能太长,否则电机和驱动电路可能因大电流发热而损坏。建议在实验中使用带限流功能的电源,并控制测试时间在几秒内。
6.3 数据波形分析思路
如果使用带波形显示的串口上位机,可以把电流数据以曲线形式显示。正常情况下电流曲线应该比较平稳,堵转时电流会出现快速上升,超载时电流会在较高位置波动。转速曲线同理,如果出现频繁跳变,则需要检查测速脉冲是否受到干扰。
从这些数据中,不仅可以判断当前电机状态,还能为后续设置更精细的报警阈值提供依据。
7. 常见问题与排错清单
| 问题现象 | 常见原因 | 排查与解决 |
|---|---|---|
| ADC 读出的 |