先问一个很实际的问题:做单片机项目时,你是怎么判断电机“现在转得正不正常”的?
很多初学者做电机类项目,只会给电机通电,让它转起来,然后发现转速不对、电流偏大、电机发烫,却完全不知道问题出在哪一步。要解决这个问题,就需要给电机加一套“体检系统”,实时检测它的电压、电流、转速和温度,再让 STM32 根据这些数据判断电机当前处于什么状态。
这篇教程就围绕“STM32 单片机电机状态检测”这个主题展开。我会从硬件方案选型开始讲,然后给出基于 STM32 的 ADC 多通道采集、电机参数计算、状态判断的完整代码,最后补充常见问题的排查思路和工程实践建议。无论是做毕业设计、课程设计,还是准备电赛、蓝桥杯嵌入式项目,这套思路都可以直接复用。
1. 电机状态检测:到底要检测什么
1.1 什么是电机状态检测
电机状态检测,简单说就是通过传感器和单片机,实时获取电机运行过程中的关键物理量,然后判断电机是否处于正常工作状态。
电机不是只要通电就会好好转的。它可能遇到这些情况:
- 电源电压跌落,电机力矩不足,转速下降。
- 电机堵转,电流急剧上升,绕组发热。
- 负载变化,导致电流波动、转速不稳定。
- 电机轴承磨损,振动异常,噪音增大。
- 长时间运行导致温度过高,可能烧毁绕组。
如果不做状态检测,这些隐患只能在电机彻底损坏之后被发现。而做好状态检测,就能提前感知异常,及时停机或报警。
1.2 状态检测的核心参数
一个比较完整的电机状态检测系统,通常关注以下几个参数:
| 参数 | 检测方式 | 反映的问题 |
|---|---|---|
| 电源电压 | 电阻分压 + ADC | 供电是否正常 |
| 工作电流 | 采样电阻 / 电流传感器 + ADC | 是否过流、堵转 |
| 转速 | 霍尔传感器 / 编码器 / 红外对射 | 是否失速、停转 |
| 温度 | NTC 热敏电阻 / DS18B20 | 是否过热 |
| 振动 | 振动传感器(如 SW-420) | 机械故障 |
对于入门项目,优先做“电压 + 电流 + 转速”三合一检测,既能覆盖大部分异常情况,硬件成本也不高。温度可以作为扩展项。
1.3 为什么用 STM32 做这个项目
很多人会问:“51 单片机不也能做 ADC 检测吗?为什么选 STM32?”
51 单片机确实可以做,但 STM32 的优势很明显:
- STM32 的 ADC 是 12 位分辨率,精度远高于 51 单片机常用的 8 位/10 位 ADC。
- STM32 支持多通道扫描,可以同时采集电压、电流、温度等多路信号。
- STM32 主频高,可以跑更复杂的滤波算法,比如滑动平均滤波、卡尔曼滤波(入门可以先不做)。
- STM32 外设丰富,可以一边采集 ADC,一边输出 PWM 控制电机,还能通过串口、CAN、OLED 显示屏和人交互。
- 做毕业设计或项目开发时,STM32 的工程资料和参考代码更多,遇到问题更容易解决。
所以,基于 STM32 做电机状态检测,既是学习单片机外设的好例子,也是一个具有工程价值的完整项目。
2. 硬件设计与方案选型
2.1 系统整体架构
整个电机状态检测系统的硬件可以分成四个部分:
- 电机驱动部分:STM32 输出 PWM,通过驱动芯片(如 L298N、TB6612、DRV8833)或 MOS 管驱动电机。
- 信号采集部分:电压采样电路、电流采样电路、转速传感器。
- 主控部分:STM32 最小系统板,负责 ADC 采集、数据处理、逻辑判断。
- 人机交互部分:OLED 显示屏显示数据,按键设置阈值,蜂鸣器报警,串口输出调试信息。
结构图可以用文字表述,大致是这样的:
电机 ← 驱动电路 ← STM32 PWM 输出 ↑ ↑ 传感器信号 → 信号调理电路 → STM32 ADC 输入 ↓ OLED / 串口 / 蜂鸣器2.2 电压检测方案
电机工作电压通常是 5V、12V 或 24V。STM32 的 ADC 输入范围一般是 0~3.3V,所以不能直接把电源电压接到 ADC 引脚。
最常用的方案是电阻分压:
Vmotor ---[R1]---+---[R2]--- GND | ADC引脚选择 R1 和 R2 的比例,让分压后的电压落在 0~3.3V 范围内。
比如检测 12V 电压,可以取 R1 = 30K,R2 = 10K,分压比是 10 / (30 + 10) = 0.25。12V × 0.25 = 3V,在 ADC 量程内。
需要注意的是,分压电阻的精度会影响测量结果,工程上最好用 1% 精度的电阻。
2.3 电流检测方案
电流检测常见有两种思路:
方法一:采样电阻法
在电机供电回路中串联一个低阻值采样电阻,比如 0.1Ω。电流流过采样电阻时会产生压降,通过运放放大后送到 ADC。
这个方案在直流电机项目中非常常用。核心公式是:
U = I × R如果电流是 1A,采样电阻是 0.1Ω,压降就是 0.1V。这个电压比较小,通常需要用运放放大后送入 ADC。
方法二:ACS712 电流传感器模块
ACS712 是 Allegro 公司生产的霍尔电流传感器,不需要串联采样电阻,可以直接把电流转换为电压输出。使用很方便,模块输出接 ADC 引脚就能采集。
ACS712 有不同量程,常见的有 5A、20A、30A 版本。用 ACS712 的话,代码里要按模块的灵敏度做换算。
2.4 转速检测方案
转速检测是电机状态检测里比较有意思的部分。常用方案:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 霍尔传感器(A3144) | 电机轴上贴磁铁,霍尔传感器感知磁场变化 | 非接触,耐用 | 需要贴磁铁 |
| 红外对射模块 | 电机轴上装遮光片,每转一圈遮挡一次 | 简单,低成本 | 容易受环境光干扰 |
| 光电编码器 | 输出两路正交脉冲 | 可以测方向,精度高 | 成本较高 |
| 电机自带霍尔输出 | 有些电机内部集成霍尔信号线 | 直接读取脉冲 | 不是所有电机都有 |
入门推荐用“霍尔传感器 + 小磁铁”,或者在转轴上贴一个黑白相间的码盘配合红外对射,然后使用 STM32 的定时器输入捕获或外部中断统计脉冲数。
转速的计算思路:
每分钟转速 = 每秒脉冲数 × 60 / 每转脉冲数比如每转产生 1 个脉冲,1 秒内数到 10 个脉冲,那么转速就是 10 × 60 / 1 = 600 RPM。
3. 环境准备与工程搭建
3.1 硬件清单
| 器件 | 说明 |
|---|---|
| STM32 开发板 | 推荐 STM32F103C8T6 最小系统板,性价比高 |
| 直流电机 | 12V 直流减速电机或普通直流电机 |
| 电机驱动模块 | TB6612 或 L298N |
| ACS712 电流传感器 | 5A 或 20A 量程均可 |
| OLED 显示屏 | 0.96 寸 SSD1306,I2C 接口 |
| 蜂鸣器模块 | 有源蜂鸣器,低电平触发或高电平触发按模块而定 |
| 电阻若干 | 做分压用 |
| 杜邦线、面包板 | 接线用 |
3.2 软件环境
我用的是比较通用的开发环境组合:
- Keil MDK5(也可以使用 STM32CubeIDE)
- STM32CubeMX(用于生成初始化代码)
- STM32 标准外设库或者 HAL 库都可以,本文示例以 HAL 库为主
如果你已经安装了 Keil5,但是还没有添加 STM32 芯片支持包,需要先在 STM32 官网下载对应芯片的 Pack 包,或者在 Keil 的 Pack Installer 里在线安装。安装完成后才能新建 STM32 工程。
版本相关的提醒:不同版本的 HAL 库在 API 名称上基本一致,但个别参数可能有差异。本文代码以最常见的 STM32F1 系列 HAL 库为例,如果你用的是其他系列,注意检查 ADC 时钟配置和引脚定义。
3.3 新建工程的整体思路
这个项目里用到的 STM32 外设主要有:
| 外设 | 用途 |
|---|---|
| GPIO | 控制电机驱动方向引脚、蜂鸣器 |
| TIM | 输出 PWM 控制电机速度 |
| ADC | 采集电压、电流信号 |
| TIM + 外部中断 | 测转速 |
| I2C | 驱动 OLED 显示 |
| USART | 串口打印调试信息 |
在 CubeMX 里,你需要做以下配置:
- 选择芯片型号,比如 STM32F103C8T6。
- 配置时钟树,将系统主频设置为 72MHz。
- 配置 ADC1,启用两个或三个通道。
- 配置一个定时器输出 PWM。
- 配置另一个定时器或外部中断引脚用于转速脉冲计数。
- 配置 I2C1 用于 OLED。
- 配置 USART1 用于串口调试。
4. 核心原理与数据处理
4.1 ADC 采样原理
STM32 的 ADC 是逐次逼近型 ADC。它将模拟电压转换为一个 12 位的数字值,范围是 0~4095。
转换公式:
ADC值 = 输入电压 / 参考电压 × 4096反过来,从 ADC 值算回电压:
输入电压 = ADC值 × 参考电压 / 4096如果参考电压是 3.3V,那么:
输入电压 = ADC值 × 3.3 / 4096示例:
ADC值 = 2048 → 输入电压 = 2048 × 3.3 / 4096 = 1.65V多通道采集时,STM32 可以配置为扫描模式,在通道之间按顺序轮询。缺点是每次只能读一个通道的结果,必须等待转换完成后再启动下一次转换。
4.2 数据处理:为什么要滤波
ADC 采集到的原始数据往往带有噪声,原因是电机运行时电刷会产生火花,引入电磁干扰。如果不做任何处理,ADC 数值会在真实值附近剧烈跳动,导致电压电流显示不准、误报警。
最简单的处理方法是滑动平均滤波。核心思路是维护一个长度为 N 的数组,每次采集到新值就放入数组末尾,去掉最前面的值,然后求平均。
滑动平均滤波的优点:
- 算法简单,计算量小,适合 STM32。
- 能有效抑制周期性噪声。
- 实时性较好。
4.3 电机状态判断逻辑
采集到电压、电流、转速之后,不能只看单个数据,还需要综合判断。
常用的判断逻辑:
如果 电流 > 过流阈值: 状态 = 过流 / 堵转 否则如果 转速 < 失速阈值: 状态 = 失速 / 停转 否则如果 电压 < 欠压阈值: 状态 = 欠压 否则: 状态 = 正常运行这里要注意优先级。比如电机堵转时,转速为 0,电流很大。此时如果先判断转速,会得到“停转”结论,但根本原因是堵转。所以应该先判断电流是否过大,再判断转速是否过低。
判断逻辑需要添加“连续多次满足条件才确认异常”的防抖机制。比如电流连续 5 次超过阈值,才认为真的过流了。这样可以避免瞬时波动导致的误判。
5. 完整实战:STM32 电机状态检测系统
下面进入本教程的核心部分。我会按照实际开发流程,从 CubeMX 配置到代码编写一步步走。
5.1 项目硬件接线
先明确接线方式。这里以 STM32F103C8T6 为例:
| 模块 | STM32 引脚 |
|---|---|
| 电机驱动 PWMA | PA0(TIM2_CH1) |
| 电机驱动 AIN1 | PA1 |
| 电机驱动 AIN2 | PA2 |
| ACS712 输出 | PA4(ADC1_IN4) |
| 电源分压输出 | PA5(ADC1_IN5) |
| OLED SCL | PB6 |
| OLED SDA | PB7 |
| 霍尔传感器信号 | PB0(外部中断) |
| 蜂鸣器 | PB1 |
| 串口 TX | PA9 |
| 串口 RX | PA10 |
注意:默认直流电机驱动中的方向引脚逻辑,请根据你实际使用的驱动模块调整。TB6612 与 L298N 的方向控制引脚电平定义可能不同。
5.2 STM32CubeMX 关键配置
在 CubeMX 中逐步完成以下设置。
RCC 配置:
选择 HSE(外部高速时钟)为 Crystal/Ceramic Resonator。
时钟树:
将 HCLK 设置为 72MHz,APB1 分频为 36MHz,APB2 分频为 72MHz。ADC 时钟不要超过 14MHz,所以 ADC 分频选择 PCLK2/6。
ADC1 配置:
- 开启通道 4(PA4)
- 开启通道 5(PA5)
- 扫描模式:Enable(多通道必须打开)
- 连续转换:Disable(这里我们用软件触发单次转换)
- 采样时间:设置为 55.5 Cycles 或更大,保证采样精度
注意:如果使用 HAL 库,多通道 ADC 还可以使用 DMA 模式。本文为了降低初学难度,先使用“连续采样 + 软件切换通道”的方式。
TIM2 配置:
- Channel1:PWM Generation CH1
- 预分频 PSC:72-1(得到 1MHz 计数频率)
- 自动重装载 ARR:9999(得到 100Hz PWM,你也可以根据电机驱动要求调整)
- 初始占空比:50%
GPIO 配置:
- PA1、PA2:输出,用于控制电机方向
- PB0:外部中断模式(上升沿触发),用于转速脉冲计数
- PB1:输出,用于控制蜂鸣器
I2C1 配置:
- 开启 I2C,默认 100kHz 即可。
USART1 配置:
- 异步模式
- 115200-8-N-1
5.3 编写代码
CubeMX 生成的代码已经完成了底层初始化。我们只需要在用户代码区域补充业务逻辑。
5.3.1 定义全局变量和数据结构
/* USER CODE BEGIN PV */ #define ADC_SAMPLE_TIMES 10 typedef enum { MOTOR_STOP = 0, MOTOR_RUNNING, MOTOR_OVER_CURRENT, MOTOR_STALL, MOTOR_UNDER_VOLTAGE } MotorState; MotorState motor_state = MOTOR_STOP; uint16_t adc_buffer[ADC_SAMPLE_TIMES][2]; uint8_t adc_index = 0; float motor_voltage = 0.0f; float motor_current = 0.0f; volatile uint32_t pulse_count = 0; volatile uint8_t speed_timer_flag = 0; float motor_speed_rpm = 0.0f; /* USER CODE END PV */5.3.2 启动外部中断和定时器
在主函数中初始化外设后,需要启动外部中断和用于测速的定时器。
这里用 SysTick 来产生 1 秒的时基,每秒计算一次转速。
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_ADC1_Init(); MX_TIM2_Init(); MX_I2C1_Init(); MX_USART1_UART_Init(); /* USER CODE BEGIN 2 */ HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); HAL_ADC_Start(&hadc1); // 使能外部中断,用于霍尔传感器脉冲计数 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); HAL_NVIC_EnableIRQ(EXTI0_IRQn); OLED_Init(); OLED_Clear(); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 5000); // 初始占空比50% HAL_UART_Transmit(&huart1, (uint8_t*)"Motor Detect System Start\r\n", 27, 100); /* USER CODE END 2 */ while (1) { // 主循环逻辑 Motor_Task(); HAL_Delay(50); } }注意:OLED_Init()是 SSD1306 显示屏驱动函数,具体实现取决于你使用的 OLED 库,这里不做展开。如果你不用 OLED,可以把显示部分去掉,只保留串口输出。
5.3.3 ADC 多通道采样函数
/* 获取指定ADC通道的值 */ uint16_t ADC_GetChannelValue(uint32_t channel) { ADC_ChannelConfTypeDef sConfig = {0}; uint16_t value = 0; sConfig.Channel = channel; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5; if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) { return 0; } HAL_ADC_Start(&hadc1); if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) { value = HAL_ADC_GetValue(&hadc1); } HAL_ADC_Stop(&hadc1); return value; }这段代码的核心思路是:每次读取之前,把 ADC 切换到指定通道。虽然效率不高,但对于电机状态检测这种低速场景完全够用。
5.3.4 滑动平均滤波
/* 滑动平均滤波,data是历史缓冲区,new_value是新采样值,n是缓冲区长度 */ uint16_t MovingAverage(uint16_t *data, uint16_t new_value, uint8_t n) { uint32_t sum = 0; uint8_t i = 0; for (i = 0; i < n - 1; i++) { data[i] = data[i + 1]; } data[n - 1] = new_value; for (i = 0; i < n; i++) { sum += data[i]; } return (uint16_t)(sum / n); }这里需要注意,sum使用uint32_t,避免 ADC 值求和时溢出。ADC 12 位最大值 4095,10 次累加最大 40950,在 uint32_t 范围内,但养成用更大类型存储求和结果的习惯是好的。
5.3.5 电压和电流换算
#define VREF 3.3f #define ADC_RES 4096.0f // 分压电阻参数:R1=30K,R2=10K #define VOLTAGE_DIVIDER_RATIO (30.0f + 10.0f) / 10.0f // ACS712 灵敏度:使用 5A 量程模块时约为 185mV/A #define ACS712_SENSITIVITY 0.185f #define ACS712_ZERO_OFFSET 2.5f float ADC_To_Voltage(uint16_t adc_value) { return (float)adc_value * VREF / ADC_RES; } float Cal_Voltage(uint16_t adc_value) { float v_adc = ADC_To_Voltage(adc_value); return v_adc * VOLTAGE_DIVIDER_RATIO; } float Cal_Current(uint16_t adc_value) { float v_adc = ADC_To_Voltage(adc_value); return (v_adc - ACS712_ZERO_OFFSET) / ACS712_SENSITIVITY; }ACS712 模块在零电流时输出电压大约是 VCC/2(2.5V),电流为正时电压升高,为负时电压降低。所以电流 = (实际电压 - 2.5V) / 灵敏度。
注意:ACS712 的灵敏度与模块量程有关,5A 模块约为 185mV/A,20A 模块约为 100mV/A,30A 模块约为 66mV/A。如果你的模块是别的量程,需要查模块数据手册。
5.3.6 转速计算
转速测量使用外部中断统计霍尔脉冲数,每秒读取一次。
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == GPIO_PIN_0) { pulse_count++; } } void Speed_Calculate(void) { if (speed_timer_flag == 1) { speed_timer_flag = 0; // 假设每转1个脉冲,如果你的电机每转多个脉冲,需要调整除数 motor_speed_rpm = (float)(pulse_count * 60); pulse_count = 0; } }这里用 SysTick 中断设置speed_timer_flag每 1 秒置 1。
在SysTick_Handler中增加:
void SysTick_Handler(void) { /* USER CODE BEGIN SysTick_IRQn 0 */ static uint32_t tick_count = 0; tick_count++; if (tick_count >= 1000) // 1秒 { tick_count = 0; speed_timer_flag = 1; } /* USER CODE END SysTick_IRQn 0 */ HAL_IncTick(); }需要注意,pulse_count是 volatile 变量,在中断中累加,在主循环中读取清零,这样在高速计数时可以减少主循环被打断的影响。
5.3.7 电机状态判断
void Motor_State_Update(void) { if (motor_current > 1.5f) // 过流阈值,按实际电机调整 { motor_state = MOTOR_OVER_CURRENT; } else if (motor_speed_rpm < 100.0f) // 失速阈值 { motor_state = MOTOR_STALL; } else if (motor_voltage < 10.5f) // 欠压阈值 { motor_state = MOTOR_UNDER_VOLTAGE; } else { motor_state = MOTOR_RUNNING; } }5.3.8 主任务函数
void Motor_Task(void) { uint16_t voltage_adc = 0; uint16_t current_adc = 0; char msg[64]; // 采集电流通道(PA4) current_adc = ADC_GetChannelValue(ADC_CHANNEL_4); current_adc = MovingAverage(&adc_buffer[0][0], current_adc, ADC_SAMPLE_TIMES); // 采集电压通道(PA5) voltage_adc = ADC_GetChannelValue(ADC_CHANNEL_5); voltage_adc = MovingAverage(&adc_buffer[0][1], voltage_adc, ADC_SAMPLE_TIMES); // 换算物理量 motor_voltage = Cal_Voltage(voltage_adc); motor_current = Cal_Current(current_adc); // 计算转速 Speed_Calculate(); // 更新状态 Motor_State_Update(); // 串口打印 sprintf(msg, "V:%.2fV I:%.2fA Speed:%.0fRPM State:%d\r\n", motor_voltage, motor_current, motor_speed_rpm, motor_state); HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), 100); // 异常报警 if (motor_state == MOTOR_OVER_CURRENT || motor_state == MOTOR_STALL) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET); // 蜂鸣器响 } else { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET); // 蜂鸣器关 } }5.4 编译与下载
在 Keil 中完成编译后,使用 ST-Link 下载程序。
下载时选择 ST-Link Debugger,如果出现下载失败,常见原因是:
- 芯片没有连接好。
- ST-Link 驱动没装好。
- Keil 中 Flash Download 配置里没有选择正确的 Flash 算法。
使用 ST-Link Utility 也可以下载,但日常开发建议在 Keil 里直接 F8 下载,配合调试功能更高效。
5.5 预期运行结果
程序下载成功后,打开串口助手,波特率设为 115200,能够看到类似下面的输出:
V:12.10V I:0.35A Speed:1200RPM State:2 V:12.08V I:0.38A Speed:1198RPM State:2 V:12.11V I:1.20A Speed:900RPM State:1 V:12.09V I:1.80A Speed:50RPM State:1这里 State 的含义:
| 值 | 含义 |
|---|---|
| 0 | 电机停止 |
| 1 | 过流/堵转 |
| 2 | 正常运行 |
| 3 | 失速 |
| 4 | 欠压 |
正常运行时,电压稳定在电源电压附近,电流较小,转速稳定在一个值。用手捏住电机轴模拟堵转,电流迅速上升,串口输出的 State 会变为过流状态,蜂鸣器报警。
6. 常见问题与排查思路
6.1 ADC 采集值跳动很大
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| ADC 数值不稳定,上下跳动几十甚至几百 | 电机干扰、采样时间过短 | 增加采样时间,改用滑动平均滤波 |
| ADC 数值整体偏高 | 参考电压不稳定 | 检查 VREF 引脚供电,使用独立 3.3V 基准 |
| 某一通道数据为零 | 通道配置错误 | 检查通道号是否对应正确引脚 |
6.2 电机不转
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 电机完全不动 | PWM 没有输出 | 检查 TIM 配置是否正确,确认占空比不是 0 |
| 电机嗡嗡响但不转 | 驱动方向引脚电平错误 | 检查 AIN1、AIN2 的高低组合 |
| 电机电压不足 | 电源功率不够 | 使用独立电机电源,不要用 STM32 USB 供电 |
6.3 转速显示为 0
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 转速一直是 0 | 霍尔传感器信号没接对 | 检查霍尔传感器输出脚是否接在 PB0 |
| 转速偶尔跳变 | 磁铁离霍尔传感器太远 | 调整磁铁和霍尔传感器的距离 |
| 转速偏大或偏小 | 每转脉冲数设置不对 | 确认电机每转产生几个脉冲,调整除法因子 |
6.4 OLED 不显示
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| OLED 黑屏 | I2C 地址不对 | 确认 SSD1306 地址是 0x3C 还是 0x3D |
| OLED 显示乱码 | 接线接触不良 | 检查 SCL、SDA 是否接反 |
| 程序卡在等待标志位 | I2C 总线被占用 | 重新上电,检查超时设置 |
6.5 电流显示负值
如果 ACS712 在没有电流时输出低于 2.5V,电流计算会得到负数。解决方法是给模块增加一个校准步骤:
float current_offset = 0.0f; void Current_Calibration(void) { uint16_t adc_value = ADC_GetChannelValue(ADC_CHANNEL_4); float v_adc = ADC_To_Voltage(adc_value); current_offset = v_adc; } float Cal_Current_Calibrated(uint16_t adc_value) { float v_adc = ADC_To_Voltage(adc_value); return (v_adc - current_offset) / ACS712_SENSITIVITY; }在电机启动前调用一次校准函数,把零电流时的输出电压记录为偏移量。
7. 最佳实践与工程建议
7.1 电源设计
电机启动瞬间电流很大,如果和 STM32 共用一个电源,会导致单片机复位或 ADC 采集异常。正确的做法是:
- 电机电源和逻辑电源分开。
- 两个电源共地。
- 电机电源加大电容,减少启动瞬间电压跌落。
如果要用一个电源给全系统供电,建议选择功率足够的电源,并在电机电源输入端并联一个大电容和一个 0.1uF 小电容。
7.2 采样稳定性
工程中提升 ADC 采样稳定性的常用方法:
- 在 ADC 引脚对地并联一个 0.1uF 滤波电容或 10nF 电容。
- 采样时间尽量长一点,比如 55.5 Cycles 或 239.5 Cycles。
- 软件上使用滑动平均滤波,不建议使用简单的单次采样。
- 对于要求更高的场景,可以考虑 DMA 采样模式加环形缓冲区。
7.3 阈值不要写死
电机参数会随着负载、电源电压变化,把阈值写死在代码里不利于项目扩展。建议:
- 在文件开头用宏定义集中管理阈值。
- 通过按键或串口指令在线修改阈值。
- 关键阈值保存到 Flash 或 EEPROM,掉电不丢失。
例如:
#define OVER_CURRENT_THRESHOLD 1.5f #define STALL_SPEED_THRESHOLD 100.0f #define UNDER_VOLTAGE_THRESHOLD 10.5f这样后续调整参数只需要修改宏定义。
7.4 状态显示要直观
不要只在串口输出状态码,建议在 OLED 上显示对应的文字状态,方便现场调试。比如:
- 运行正常
- 电流过大
- 电机堵转
- 电压偏低
同时可配合不同颜色的 LED 或蜂鸣器声音频率做区分。
7.5 添加保护动作
真正的电机保护系统不能只报警,还需要自动停机。在检测到过流或堵转时,应立即关闭 PWM 输出,否则电机可能烧毁。
if (motor_state == MOTOR_OVER_CURRENT || motor_state == MOTOR_STALL) { HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_1); // 也可以直接设置占空比为0 __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 0); } else { HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); }7.6 调试信息分级
开发阶段可以通过串口输出所有调试信息,比如 ADC 原始值、滤波后的值、计算后的物理量、状态判断结果。但在实际项目部署时,应减少无用的日志输出,避免大量串口打印占用 CPU 时间。
8. 总结与学习路线
到这里,一个基于 STM32 的电机状态检测系统就完整落地了。
回顾一下完成的工作:
- 理解了电机状态检测的常见参数和应用场景。
- 掌握了电压分压采集、电流传感器、霍尔测速的硬件方案。
- 在 STM32F103 上配置了 ADC 多通道采集。
- 添加了滑动平均滤波,提高了数据稳定性。
- 实现了电压、电流、转速的物理量换算。
- 编写了电机状态判断逻辑和报警保护功能。
- 通过串口和 OLED 实时显示电机运行状态。
如果你是用 51 单片机做类似项目,原理也相通,只是 ADC 精度和代码组织上要适当调整。
接下来想继续深入,可以从这几个方向选一个:
- 学习 PID 闭环控制,把状态检测和电机调速结合起来。
- 学习 CAN 总线通信,把状态数据上传到上位机或车载网络。
- 学习 FreeRTOS,把 ADC 采集、数据显示、状态判断放到不同任务中运行。
- 学习无刷直流电机(BLDC)控制,用 STM32 检测反电动势实现电机换相。
电机状态检测是嵌入式开发中一个很典型的多外设综合项目,它把 GPIO、定时器、ADC、中断、串口、显示全都串起来了。做完这个项目,你对 STM32 的整体掌握程度会上一个台阶。建议自己动手把硬件搭起来,亲手改一遍代码,观察不同工况下的数据变化,这比只看文章理解得深刻得多。