基于STM32的电机状态检测系统:电流、转速与温度测量实战
电机长时间运行后电流异常、转速下降、温度升高这些小毛病往往比突然停机更让人头疼。项目里需要实时掌握“电机现在到底怎么样”不能每次都拿万用表去量、拿转速表去对。基于 STM32 单片机做一套电机状态检测系统能把电流、转速、温度这些关键参数采集上来再通过串口或显示屏输出既方便调试也为后续做故障预警、PID 调速留好了底层数据。本文以 STM32F103C8T6 为核心围绕电机状态检测的硬件选型、检测原理、代码实现、常见排查思路展开。适合正在做单片机课设、毕业设计或者刚接触电机控制的开发者阅读。跟着文章一步步做你能掌握 ADC 采集、外部中断测速、单总线温度传感器读取这些基础而实用的功能最后组合出一套可以跑起来的电机状态检测方案。1. 项目背景与核心概念1.1 什么是电机状态检测电机状态检测简单来说就是通过传感器和单片机采集电机运行时的关键物理量并进一步判断电机是否处于正常工作范围。电机在运行过程中比较能反映状态的特征量主要有四个电流电机负载变化时电流会明显波动堵转、短路时电流会急剧上升。转速超过额定转速或转速波动大往往意味着负载异常或机械故障。温度电机绕组温度过高会加速绝缘老化严重时直接烧毁。振动 / 噪声多用于大型电机需要加速度传感器单片机项目里相对少用。对于入门级 STM32 项目最常用的是电流、转速、温度三个维度。它们分别对应 ADC 采样、脉冲计数、单总线或模拟量温度传感器刚好覆盖了 STM32 常用的外设功能。1.2 为什么选择 STM32 来做以前用 51 单片机也能做类似检测但 STM32 的优势在于ADC 分辨率高、支持多通道采样电机电流和电压更平滑。定时器资源丰富可以做输入捕获、编码器接口转速测量方式灵活。主频高、外设库完善后续如果要加 PID 调速、CAN 通信、FreeRTOS 实时系统性能余量更充足。支持标准外设库和 HAL 库两种开发方式网上资料多遇到问题好排查。所以把电机状态检测作为 STM32 入门到进阶的实践项目非常合适。1.3 本文项目范围本文不涉及复杂的电机驱动算法也不是完整变频器方案而是聚焦“检测”本身采集直流电机工作电流和电压。通过霍尔传感器或红外对管测量电机转速。读取电机表面温度。将数据实时显示在 OLED 屏上并通过串口输出到上位机。这套系统可以用在小型风机、水泵、桌面小车等场景也是后续做电机故障诊断、远程监控的底层雏形。2. 环境准备与硬件选型2.1 硬件清单模块推荐型号作用主控STM32F103C8T6 最小系统板数据采集与处理电机直流减速电机或空心杯电机带霍尔被检测对象电机驱动L298N / TB6612 / DRV8833驱动电机转动电流检测ACS712 电流传感器模块或采样电阻方案采集电流转速检测霍尔传感器模块如 3144 磁钢或光电对管输出脉冲测速温度检测DS18B20 防水探头采集电机表面/环境温度显示0.96 寸 OLEDI2C 接口本地显示通信USB-TTL 模块串口输出到电脑需要注意ACS712 模块输出的是模拟电压接 STM32 的 ADC 引脚即可。霍尔测速模块输出的是数字脉冲需要用外部中断或定时器输入捕获来统计频率。2.2 软件环境本文示例代码以 HAL 库为例因为 STM32CubeMX 生成工程的方式比较通用新老用户都能快速上手。开发环境Keil MDK 或 STM32CubeIDE 均可。初始化配置STM32CubeMX 6.0 以上版本。调试工具ST-Link 或 USB-TTL 串口调试助手。依赖库STM32F1xx HAL 库通过 CubeMX 自动生成不需要手动移植。版本需要根据你的项目实际情况调整本文重点演示配置思路。如果已经熟悉标准外设库也可以参考代码逻辑改写成标准库版本。2.3 工程结构规划为了方便阅读建议在 CubeMX 生成工程后把代码分成几个独立模块Core/ ├── Inc/ │ ├── main.h │ ├── adc_motor.h │ ├── hall_speed.h │ └── ds18b20.h └── Src/ ├── main.c ├── adc_motor.c ├── hall_speed.c └── ds18b20.c这样的好处是每个传感器对应一个源文件互不干扰后续想换传感器或者扩展功能只需要修改对应模块。3. 电机状态检测原理拆解3.1 电流检测ADC 采样与换算ACS712 电流传感器内部有霍尔元件电流流过时会产生与电流大小成正比的电压输出。输出公式大致为输出电压 2.5V 灵敏度 × 电流以 ACS712-5A 为例灵敏度约 185mV/A。也就是说电流为 0A 时输出约 2.5V。电流为 1A 时输出约 2.685V。电流为 -1A 时输出约 2.315V。STM32 的 ADC 是 12 位分辨率参考电压一般为 3.3V。采样得到的 ADC 原始值需要转换成电压再由电压换算出电流。比如 ADC 原始值是 2048参考电压 3.3V那么电压 2048 / 4095 × 3.3 ≈ 1.650V这个结果显然不对因为 ACS712 输出在 0A 时应该是 2.5V。这里一定要做减法电流 (电压 - 2.5) / 0.1853.2 转速检测脉冲计数法电机转轴上安装一个磁钢霍尔传感器靠近时每个磁极经过都会输出一个脉冲。单位时间内统计脉冲数就能换算成转速。如果电机每转一圈产生 N 个脉冲1 秒内统计到的脉冲数为 M那么转速 M / N × 60 转/分钟在 STM32 中比较可靠的做法是用外部中断 EXTI 检测霍尔传感器引脚上升沿。每次触发中断脉冲计数变量加 1。用定时器每 1 秒产生一次中断在中断里读取当前脉冲数并计算转速。这里有个容易被忽略的问题外部中断里不能做太多运算否则会影响下一次脉冲检测。正确做法是只做简单计数转速计算放到定时器中断或主循环。3.3 温度检测DS18B20 单总线协议DS18B20 是数字温度传感器使用单总线通信只需要一根数据线。它返回的是 12 位温度数据温度值 原始数据 × 0.0625。单总线通信时序比较严格需要按顺序执行初始化复位脉冲。发送 ROM 指令如跳过 ROM0xCC。发送功能指令如启动温度转换0x44。等待转换完成后再读取暂存器0xBE。因为 STM32 的 GPIO 有多种输出模式做单总线时必须把引脚配置为开漏输出并接上拉电阻。用 HAL 库时需要灵活切换输入输出模式所以这部分代码比 ADC 和测速要多一些。3.4 为什么需要安全隔离这个项目里电机如果使用大电流电源或交流供电就不能让 STM32 的 GND 和电机电源直接共地否则电机的启停电流会干扰单片机严重时甚至烧毁控制板。实操中常用两种方式使用光耦隔离霍尔信号通过光耦再进入 STM32驱动信号通过光耦再进驱动器。使用带隔离的电流传感器比如全隔离型电流变送器。对于入门项目如果只驱动小型直流电机可以用“电机电源和单片机电源分开、共地采用单点连接”的方式降低成本但一定要在电机电源两端加续流二极管和去耦电容。4. 完整实战案例基于 STM32F103C8T6 的电机状态检测系统下面从 CubeMX 配置到代码编写完整走一遍。示例以 STM32F103C8T6 和 HAL 库为基础。4.1 引脚分配功能引脚说明电机 PWM 控制PA8定时器 TIM1 的通道 1输出 PWM电机方向控制PB12 / PB13GPIO 输出电流采样 ADCPA4ADC1_IN4霍尔测速输入PA1外部中断 EXTI1转速统计定时器TIM31 秒中断DS18B20 数据线PB5开漏输出 上拉OLED I2CPB8 / PB9I2C1_SCL / I2C1_SDA串口调试PA9 / PA10USART1_TX / USART1_RX4.2 STM32CubeMX 工程配置打开 STM32CubeMX选择芯片 STM32F103C8T6然后进行以下配置在 Pinout 界面把 PA8 配置为 TIM1_CH1PA1 配置为 GPIO_EXTI1PA4 配置为 ADC1_IN4PB5 配置为 GPIO_OutputPB8/PB9 配置为 I2C1PA9/PA10 配置为 USART1。打开 ADC1勾选 IN4配置为单通道扫描模式采样时间可以拉到最大如 239.5 Cycles减少采样的内阻影响。配置 TIM1 的通道 1 为 PWM Generation CH1PWM 频率设置为 20kHz 左右避免电机啸叫。配置 TIM3选择 Internal ClockPrescaler 和 Period 算好让中断时间为 1 秒。比如 72MHz 主频PSC 设为 7199ARR 设为 9999即可产生 1Hz 中断。配置 USART1波特率 115200。在 GPIO 设置里把 PB5 初始电平设为 High保证 DS18B20 上电时数据线处于空闲状态。生成工程工具链选择 MDK-ARM 或 STM32CubeIDE。这里有一个容易踩坑的地方PA1 作为外部中断CubeMX 默认会生成 HAL_GPIO_EXTI_Callback 的空回调函数你需要自己重写它。4.3 ADC 电流采集模块ADC 模块的核心功能是每隔一段时间采集一次 ACS712 的输出电压换算成电流值。先看头文件// 文件路径Core/Inc/adc_motor.h #ifndef ADC_MOTOR_H #define ADC_MOTOR_H #include main.h #define ACS712_SENSITIVITY 0.185f // ACS712-5A 灵敏度单位 V/A #define ACS712_ZERO_VOLTAGE 2.5f // 0A 时输出电压单位 V #define ADC_REF_VOLTAGE 3.3f // STM32 ADC 参考电压 #define ADC_RESOLUTION 4095.0f // 12 位 ADC 最大值 void ADC_Motor_Init(void); float ADC_Motor_GetCurrent(void); float ADC_Motor_GetVoltage(void); #endif再看源文件// 文件路径Core/Src/adc_motor.c #include adc_motor.h #include adc.h #define ADC_SAMPLE_TIMES 20 uint32_t ADC_Motor_ReadValue(void) { uint32_t sum 0; for (uint8_t i 0; i ADC_SAMPLE_TIMES; i) { HAL_ADC_Start(hadc1); if (HAL_ADC_PollForConversion(hadc1, 10) HAL_OK) { sum HAL_ADC_GetValue(hadc1); } HAL_Delay(1); } return sum / ADC_SAMPLE_TIMES; } float ADC_Motor_GetVoltage(void) { uint32_t adc_value ADC_Motor_ReadValue(); float voltage (adc_value / ADC_RESOLUTION) * ADC_REF_VOLTAGE; return voltage; } float ADC_Motor_GetCurrent(void) { float voltage ADC_Motor_GetVoltage(); float current (voltage - ACS712_ZERO_VOLTAGE) / ACS712_SENSITIVITY; return current; }这段代码为什么做多次采样取平均因为电机工作时电流会有纹波单次采样容易跳到异常值。20 次采样取平均后数据会稳定很多。实际使用中要注意如果电流传感器接的不是 ACS712 而是采样电阻运算放大器方案需要把灵敏度参数改成你自己的换算系数。4.4 转速测量模块转速模块用外部中断和定时器配合实现。先定义全局计数变量和接口// 文件路径Core/Inc/hall_speed.h #ifndef HALL_SPEED_H #define HALL_SPEED_H #include main.h #define HALL_PULSES_PER_REV 2 // 每圈脉冲数根据实际安装调整 void Hall_Speed_Init(void); void Hall_Speed_Update(void); uint16_t Hall_Speed_GetRPM(void); #endif// 文件路径Core/Src/hall_speed.c #include hall_speed.h volatile uint32_t hall_pulse_count 0; static uint32_t last_pulse_count 0; static volatile uint16_t motor_rpm 0; void Hall_Speed_Update(void) { uint32_t capture_count hall_pulse_count; if (capture_count last_pulse_count) { motor_rpm (uint16_t)((capture_count - last_pulse_count) * 60 / HALL_PULSES_PER_REV); } else { // 计数器回绕保护极少出现 motor_rpm 0; } last_pulse_count capture_count; } uint16_t Hall_Speed_GetRPM(void) { return motor_rpm; }然后在主文件里重写外部中断回调函数// 文件路径Core/Src/main.c用户代码区 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin HALL_SPEED_Pin_Pin) { hall_pulse_count; } }这里说明一下HALL_SPEED_Pin_Pin 是 CubeMX 根据 P1 引脚名生成的宏如果你的引脚名不同需要修改为对应宏。在定时器中断回调函数里调用更新时间void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { Hall_Speed_Update(); } }这样每隔 1 秒motor_rpm 就会被刷新一次。4.5 DS18B20 温度读取模块DS18B20 代码相对长一些核心是单总线时序。下面给出精简版可以直接复制使用。// 文件路径Core/Inc/ds18b20.h #ifndef DS18B20_H #define DS18B20_H #include main.h uint8_t DS18B20_Start(void); int16_t DS18B20_GetTemperature(void); #endif// 文件路径Core/Src/ds18b20.c #include ds18b20.h #include gpio.h #define DS18B20_GPIO_PORT GPIOB #define DS18B20_GPIO_PIN GPIO_PIN_5 // 控制引脚输出 static void DS18B20_SetOutput(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DS18B20_GPIO_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DS18B20_GPIO_PORT, GPIO_InitStruct); } // 控制引脚输入 static void DS18B20_SetInput(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DS18B20_GPIO_PIN; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(DS18B20_GPIO_PORT, GPIO_InitStruct); } static void DS18B20_DelayUs(uint16_t us) { // 简单延时72MHz 下大约延时 us 微秒 for (uint16_t i 0; i us; i) { for (uint8_t j 0; j 8; j) { __NOP(); } } } uint8_t DS18B20_Reset(void) { uint8_t presence 0; DS18B20_SetOutput(); HAL_GPIO_WritePin(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN, GPIO_PIN_RESET); DS18B20_DelayUs(500); HAL_GPIO_WritePin(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN, GPIO_PIN_SET); DS18B20_DelayUs(30); DS18B20_SetInput(); presence HAL_GPIO_ReadPin(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN); // DS18B20 拉低总线表示存在 if (presence GPIO_PIN_SET) { return 1; } DS18B20_DelayUs(400); return 0; } static void DS18B20_WriteByte(uint8_t data) { DS18B20_SetOutput(); for (uint8_t i 0; i 8; i) { if (data (1 i)) { HAL_GPIO_WritePin(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN, GPIO_PIN_RESET); DS18B20_DelayUs(2); HAL_GPIO_WritePin(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN, GPIO_PIN_SET); DS18B20_DelayUs(60); } else { HAL_GPIO_WritePin(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN, GPIO_PIN_RESET); DS18B20_DelayUs(60); HAL_GPIO_WritePin(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN, GPIO_PIN_SET); DS18B20_DelayUs(2); } } } static uint8_t DS18B20_ReadByte(void) { uint8_t data 0; DS18B20_SetInput(); for (uint8_t i 0; i 8; i) { DS18B20_SetOutput(); HAL_GPIO_WritePin(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN, GPIO_PIN_RESET); DS18B20_DelayUs(2); HAL_GPIO_WritePin(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN, GPIO_PIN_SET); DS18B20_DelayUs(2); DS18B20_SetInput(); if (HAL_GPIO_ReadPin(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN) GPIO_PIN_SET) { data | (1 i); } DS18B20_DelayUs(60); } return data; } int16_t DS18B20_GetTemperature(void) { uint8_t temp_lsb, temp_msb; int16_t raw_temp; float temperature; if (DS18B20_Reset() ! 0) { return -1000; // 传感器未接 } // 跳过 ROM DS18B20_WriteByte(0xCC); // 启动温度转换 DS18B20_WriteByte(0x44); DS18B20_Reset(); DS18B20_WriteByte(0xCC); // 读取暂存器 DS18B20_WriteByte(0xBE); temp_lsb DS18B20_ReadByte(); temp_msb DS18B20_ReadByte(); raw_temp (temp_msb 8) | temp_lsb; // 12 位分辨率温度 原始值 * 0.0625 temperature raw_temp * 0.0625f; return (int16_t)temperature; }这段代码的延时函数是近似实现如果主频不是 72MHz时序会有偏差。更严谨的做法是使用定时器微秒延时但上述代码在标准 72MHz 下多数情况可以工作。4.6 主程序综合逻辑主循环里按键或直接按默认状态执行检测然后显示和输出。// 文件路径Core/Src/main.c主循环 while (1) { float current ADC_Motor_GetCurrent(); uint16_t rpm Hall_Speed_GetRPM(); int16_t temp DS18B20_GetTemperature(); // 通过串口输出 char msg[64]; sprintf(msg, I:%.2fA RPM:%d T:%d.%dC\r\n, current, rpm, temp / 10, temp % 10); HAL_UART_Transmit(huart1, (uint8_t *)msg, strlen(msg), 100); // OLED 显示可以调用 OLED_ShowString 等函数 OLED_ShowString(0, 0, (uint8_t *)Motor Monitor); OLED_ShowNum(0, 2, (uint16_t)(current * 100), 4); OLED_ShowNum(0, 4, rpm, 4); OLED_ShowNum(0, 6, (int16_t)temp, 4); HAL_Delay(500); }上面用到了 sprintf需要包含头文件 stdio.h。OLED 部分的函数名以实际移植的库为准不同库函数名略有差异这里只展示调用思路。4.7 运行与验证把程序烧录到 STM32 后连接好电机和传感器打开串口调试助手波特率设为 115200。电机空载时电流应该在几十到几百毫安范围内波动转速在额定值附近温度从室温慢慢上升。如果电机负载增大比如用手轻轻捏住减速电机输出轴电流应该明显上升转速下降。这时候就说明状态检测系统正常工作。5. 常见问题与排查思路问题现象常见原因解决思路ADC 采集的电流值一直为 0ACS712 供电不对或输出未接到 PA4万用表测量 ACS712 输出是否为 2.5V 左右电流值跳变很大电机 PWM 干扰和电源纹波采样取平均加 100nF 电容合理布线转速始终为 0霍尔传感器供电、接线、磁钢方向看霍尔模块是否有 LED 闪动确认每圈脉冲数转速数值偏大或偏小HALL_PULSES_PER_REV 配置不对手动转一圈数一下实际脉冲次数DS18B20 返回 -1000传感器未接或接线错误检查上拉电阻、数据线引脚程序运行后电机一启动就复位电源跌落或地线干扰电机电源与单片机电源分开加滤波电容OLED 不显示I2C 地址不对或引脚配置错一般 SSD1306 地址为 0x3C 或 0x3D逐一排查外部中断不触发EXTI 回调函数没重写确认 HAL_GPIO_EXTI_Callback 是否被调用其中“电机一启动就复位”是很多初学者最容易遇到的问题。根本原因往往不是代码逻辑错误而是供电问题电机启动瞬间电流很大拉低了整个电源电压导致 STM32 复位。解决方法是把电机和单片机分开供电并且共地点尽量靠近电源输入端。6. 最佳实践与工程建议6.1 信号采样的抗干扰设计电机是典型的电磁干扰源单片机检测电机状态时最容易受影响的是 ADC 采样值。建议按以下顺序处理干扰电源端电机电源和单片机电源分开电机电源两端并联 100uF 电解电容和 104 陶瓷电容。信号线电流传感器输出线和霍尔信号线使用双绞线或屏蔽线屏蔽层单端接地。软件滤波对 ADC 采样值做多次平均或一阶低通滤波。地线所有传感器和单片机的 GND 单点汇合避免形成地环路。6.2 代码工程规范这个项目虽然不大但从一开始就养成良好习惯后面扩展很关键。每个传感器一个模块不要在 main.c 里堆所有逻辑。硬件相关参数比如 HALL_PULSES_PER_REV、ACS712_SENSITIVITY用宏定义集中管理。中断服务函数里只做标志位置位或简单计数复杂计算放到主循环或低优先级回调中。串口输出可以做简单的协议比如帧头 数据 校验方便后续上位机解析。6.3 安全边界与合法授权如果检测的是市电供电的交流电机项目性质就变了需要非常谨慎严禁在通电状态下触碰电机接线端子。STM32 与强电部分必须使用隔离器件不能直接共地。明确项目仅用于实验室测试或授权环境下的教学验证不要私自在非授权设备上改装。电机堵转电流很大检测程序里可以设置阈值超过阈值后自动关闭 PWM 输出起到保护作用。比如在主循环里加一个简单判断if (current 3.0f) { HAL_TIM_PWM_Stop(htim1, TIM_CHANNEL_1); HAL_GPIO_WritePin(MOTOR_PWM_GPIO_Port, MOTOR_PWM_Pin, GPIO_PIN_RESET); }这本质上就是一个最小的过流保护逻辑实际项目中还可以用比较器硬件实现更快响应软件作为后备。6.4 日志记录与调试技巧建议在调试阶段打开串口输出每隔 500ms 输出一次电流、转速、温度方便对比异常。等系统稳定后再关闭串口输出减少干扰。如果使用 ST-Link还可以在 Keil 的 Debug 窗口直接查看 hall_pulse_count 变量排查外部中断是否触发。这个方法比加打印更高效。7. 总结与后续扩展方向本篇文章围绕“STM32 单片机电机状态检测”这个主题从项目背景、硬件选型、检测原理、CubeMX 配置到代码实现完整走了一遍。你现在应该能理解电机状态检测主要测哪些量分别用什么外设去测。如何用 ADC 采集电流传感器的模拟量并换算成电流。如何用外部中断 定时器统计脉冲来测速。如何用 DS18B20 单总线读取温度。遇到系统复位、数据跳变时优先从哪里排查。把这个基础项目做完之后可以继续往三个方向深入增加控制环节根据检测到的转速和电流配合 PID 算法实现电机闭环调速这是从“检测”走向“运动控制”的关键一步。增加通信能力把检测数据通过 CAN 总线或 RS485 上报给上位机做多电机分布式监控。增加故障诊断把电流、转速、温度数据录下来通过阈值判断或简单的趋势分析提前预警电机异常。项目练习时建议重点验证电流异常保护逻辑和测速准确性这两个点直接关系到系统可靠性。如果本文对你有帮助可以收藏备用后续调试电机驱动和传感器时随时回来看。