STM32 ADC与PWM实战:电位器控制舵机角度实现详解
这次我们来看一个在嵌入式开发中非常经典且实用的项目:使用STM32微控制器,通过电位器(模拟输入)来实时控制舵机(PWM输出)。这个项目看似基础,却是理解STM32 ADC(模数转换)和PWM(脉冲宽度调制)两大核心外设协同工作的绝佳案例。它广泛应用于机器人关节控制、云台角度调节、智能小车转向等场景。
对于刚接触STM32的开发者来说,直接从电位器读取电压值,并实时映射为舵机的PWM信号,这个过程能让你快速掌握HAL库或标准库对这两个外设的配置与调用。本文将带你从零开始,基于STM32CubeMX和Keil MDK(或STM32CubeIDE)开发环境,一步步实现这个功能。我们会重点关注硬件连接、CubeMX配置、代码逻辑以及实际调试中的关键点,确保你不仅能“跑通”,更能“理解”。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 核心功能 | 通过旋转电位器,实时、线性地控制舵机转动角度。 |
| 关键技术 | STM32的ADC(模拟数字转换器)采集电位器电压;TIM(定时器)产生PWM波控制舵机。 |
| 主要硬件 | STM32最小系统板(如F103C8T6)、电位器(10kΩ)、舵机(如SG90/MG996R)、杜邦线、电源。 |
| 开发环境 | STM32CubeMX(配置工具)、Keil MDK / STM32CubeIDE(编译环境)、ST-LINK/V2(下载调试器)。 |
| 代码基础 | 使用STM32 HAL库或标准库,代码结构清晰,易于移植到不同型号STM32。 |
| 适合场景 | 嵌入式入门学习、机器人原型开发、需要模拟量输入控制执行机构的项目。 |
| 学习价值 | 深入理解ADC采样、PWM原理、HAL库外设配置流程及两者协同工作的编程思想。 |
2. 适用场景与使用边界
这个项目非常适合以下几类开发者和应用场景:
- STM32初学者:作为第一个综合性的外设应用实验,将GPIO、ADC、TIM等知识点串联起来。
- 机器人爱好者:用于控制机械臂、摄像头云台、机器人头部转动等需要精确角度控制的关节。
- 智能硬件原型:在需要手动旋钮调节设备参数(如灯光亮度、电机速度、阀门开度)的场合,舵机可替换为其他PWM驱动设备。
- 教学演示:直观展示模拟信号到数字信号,再到控制信号的完整链路。
使用边界与注意事项:
- 精度限制:ADC的采样精度(如12位)和电位器的线性度决定了控制的平滑度和精度。对于高精度应用,需考虑使用更高精度的ADC或数字传感器(如编码器)。
- 舵机性能:不同舵机(如SG90和MG996R)的扭矩、速度、工作电压不同,需根据负载选择合适的型号,并确保供电充足(特别是大扭矩舵机需独立供电)。
- 实时性:本项目采用“采集-转换-更新”的循环方式,在简单应用中足够。但对于需要极快响应速度或复杂控制算法(如PID)的场景,需考虑使用中断或DMA方式传输ADC数据。
- 安全操作:连接电路时务必断电操作,避免短路。舵机堵转(机械结构卡住但电机仍在尝试转动)会产生大电流,可能损坏舵机或驱动电路,设计时应考虑机械限位或电流保护。
3. 环境准备与前置条件
在开始编码前,请确保你的软硬件环境已就绪。
硬件清单:
- STM32开发板:一块常见的STM32核心板,如基于Cortex-M3内核的STM32F103C8T6(蓝色小板),它资源足够且社区资料丰富。
- 电位器:一个10kΩ的直滑或旋转电位器。
- 舵机:一个标准舵机,如SG90(小扭矩,适用于演示)或MG996R(扭矩较大)。注意其工作电压(通常4.8V-6.8V)。
- 连接线:若干杜邦线(公对公、母对母)。
- 电源:为STM32开发板供电的USB线,以及为舵机供电的电源(可使用开发板上的5V引脚为SG90供电,但负载重时建议外接)。
- 下载调试器:ST-LINK/V2或兼容的调试器。
软件清单:
- STM32CubeMX:ST官方推出的图形化配置工具,用于初始化芯片时钟、外设等,生成工程框架。务必安装对应你STM32系列的芯片支持包(DFP)。
- 集成开发环境(IDE):
- Keil MDK-ARM (uVision):传统且强大的商业IDE,有代码大小限制。
- STM32CubeIDE:ST官方推出的免费IDE,基于Eclipse,集成CubeMX功能,推荐新手使用。
- 串口调试助手:如XCOM、SSCOM、Putty等,用于查看ADC采样值等调试信息(可选,但强烈推荐)。
4. 硬件连接与电路原理
正确的硬件连接是项目成功的第一步。我们以STM32F103C8T6为例进行连接。
电路连接示意图:
电位器 -> STM32 -> 舵机具体引脚连接:
电位器连接(模拟输入):
- 电位器三个引脚:两端接VCC(3.3V)和GND,中间滑动引脚接STM32的某个ADC输入通道对应的GPIO,例如PA0(对应ADC1的通道0)。
- 原理:旋转电位器,中间引脚电压在0-3.3V之间变化,STM32的ADC将此模拟电压转换为数字值。
舵机连接(PWM输出):
- 舵机有三根线:
- 棕色/黑色->GND(接开发板GND)
- 红色->VCC(接5V电源。注意:STM32的IO电压是3.3V,但舵机控制信号是3.3V兼容的,供电需5V左右)
- 橙色/黄色(信号线)-> STM32的某个定时器通道对应的GPIO,例如PA8(本例中使用TIM1的通道1产生PWM)。
- 原理:舵机通过信号线上的PWM脉冲宽度来识别目标角度。标准PWM周期为20ms(50Hz),脉冲宽度在0.5ms到2.5ms之间对应0°到180°。
- 舵机有三根线:
电源注意:如果使用SG90这样的小舵机,可以直接从开发板的5V引脚取电。如果使用多个舵机或MG996R等大扭矩舵机,务必使用独立的外接5V电源为舵机供电,并与STM32共地,以避免开发板稳压芯片过载导致复位。
5. STM32CubeMX工程配置
我们将使用STM32CubeMX来快速生成一个配置好时钟、ADC和PWM的工程。
步骤 1:创建新工程打开CubeMX,点击“New Project”。在芯片选择器中输入你的型号,如STM32F103C8Tx,选中具体型号后点击“Start Project”。
步骤 2:配置系统核心(SYS)在“Pinout & Configuration”标签页左侧,找到“System Core” -> “SYS”。
- 将“Debug”设置为
Serial Wire。这对于ST-LINK调试是必须的。
步骤 3:配置时钟(RCC)找到“System Core” -> “RCC”。
- 将“High Speed Clock (HSE)”设置为
Crystal/Ceramic Resonator。这告诉芯片我们使用了外部高速晶振。
步骤 4:配置ADC
- 在左侧“Analog”下找到“ADC1”。
- 在右侧“Configuration”标签中,进入“Parameter Settings”:
Scan Conversion Mode:Disabled(我们只用一个通道)Continuous Conversion Mode:Enabled(使能连续转换模式,自动连续采样)Discontinuous Conversion Mode:DisabledEnd Of Conversion Selection:EOC flag at the end of single conversionClock Prescaler: 根据系统时钟选择合适分频,确保ADC时钟不超过14MHz(对于F1系列)。Resolution:12-bit(转换结果范围0-4095)Data Alignment:Right alignmentSampling Time: 选择一个适当的周期,如Cycles 239.5,采样时间越长精度一般越高,但转换速度越慢。
- 进入“NVIC Settings”标签:
- 暂时不使能ADC全局中断。我们先用轮询方式读取。
步骤 5:配置PWM(使用定时器)
- 在左侧“Timers”下找到一个高级或通用定时器,例如
TIM1。 - 在芯片图形界面上,点击
TIM1的CH1引脚(例如PA8),选择PWM Generation CH1。 - 在右侧“Parameter Settings”中配置:
Prescaler (PSC): 计算值,用于分频定时器时钟。目标是产生50Hz(20ms周期)的PWM。Counter Mode:UpCounter Period (ARR): 计算值,与PSC共同决定PWM频率。auto-reload preload:Enabled
- 如何计算PSC和ARR?
- 假设系统时钟
SYSCLK = 72MHz。 - 我们希望PWM频率为50Hz,即周期
T = 1/50 = 0.02s = 20,000 us。 - TIM1的时钟
TIM_CLK = SYSCLK = 72MHz。 - 定时器计数一次的时间
t = 1 / TIM_CLK * (PSC + 1)。 - 整个周期对应的计数值
ARR = T / t = T * TIM_CLK / (PSC + 1)。 - 为了得到整数ARR,我们可以先设定一个PSC。例如,令
PSC = 7199,则t = 1/72MHz * 7200 = 0.0001s = 100us。 - 那么
ARR = 20,000us / 100us = 200。 - 所以,设置
PSC = 7199,ARR = 199(因为从0开始计数)。这样产生的PWM频率就是72MHz / (7200 * 200) = 50Hz。
- 假设系统时钟
- 在下方的“PWM Generation Channel 1”配置中:
Pulse: 初始脉冲宽度值,对应0.5ms。计算:Pulse = (0.5ms / 100us) = 5。我们先设为5。
步骤 6:配置时钟树点击上方“Clock Configuration”标签。
- 通常使用HSE(8MHz)作为时钟源,通过PLL倍频到72MHz(对于F103系列是常用最高频率)。具体步骤:选择HSE作为PLL源,设置PLL倍频因子为9,系统时钟选择PLL输出。APB1总线时钟最高36MHz,APB2总线时钟最高72MHz。CubeMX会自动帮你计算并设置好分频系数。
步骤 7:生成工程
- 点击上方“Project Manager”标签。
- “Project”子标签中,设置工程名称、路径、IDE(选择
MDK-ARM V5或STM32CubeIDE)。 - “Code Generator”子标签中,建议选择:
Copy only the necessary library filesGenerate peripheral initialization as a pair of ‘.c/.h’ files per peripheralSet all free pins as analog (to optimize power consumption)
- 点击右上角的“GENERATE CODE”,生成工程文件并打开IDE。
6. 核心代码编写与逻辑实现
工程生成后,我们主要需要在main.c文件中添加应用逻辑代码。关键点在于读取ADC值,并将其映射为控制舵机的PWM脉冲宽度。
步骤 1:启动ADC与PWM在main()函数中,系统初始化后,HAL库已经帮我们初始化了ADC和TIM。我们需要手动启动它们。
/* USER CODE BEGIN 2 */ // 启动ADC连续转换 HAL_ADC_Start(&hadc1); // 启动TIM1的PWM输出通道1 HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); /* USER CODE END 2 */步骤 2:主循环中读取ADC并更新PWM在while (1)主循环中,我们不断读取ADC值,进行计算,并更新PWM的占空比。
/* USER CODE BEGIN WHILE */ uint32_t adc_value = 0; uint16_t servo_pulse = 0; while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ // 1. 读取ADC值(轮询方式) HAL_ADC_PollForConversion(&hadc1, 50); // 等待转换完成,超时50ms adc_value = HAL_ADC_GetValue(&hadc1); // 获取12位ADC值,范围0-4095 // 2. 将ADC值映射到舵机脉冲宽度 // ADC范围: 0 ~ 4095 // 舵机PWM脉冲宽度范围: 0.5ms ~ 2.5ms (对应0° ~ 180°) // 定时器一个计数周期是100us (见CubeMX配置) // 所以脉冲计数值范围: 5 (0.5ms/100us) ~ 25 (2.5ms/100us) // 使用线性映射公式: y = y1 + (x - x1) * (y2 - y1) / (x2 - x1) servo_pulse = 5 + (adc_value * (25 - 5)) / 4095; // 3. 更新TIM1通道1的脉冲值,从而改变舵机角度 __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, servo_pulse); // 4. 可选:通过串口打印ADC值和脉冲值,用于调试 // printf("ADC: %lu, Pulse: %u\r\n", adc_value, servo_pulse); // 需要事先重定向printf到串口 HAL_Delay(10); // 延时10ms,控制更新频率,避免过于频繁 } /* USER CODE END 3 */代码逻辑详解:
HAL_ADC_PollForConversion&HAL_ADC_GetValue:由于我们在CubeMX中配置了ADC为连续转换模式,这里使用轮询方式读取最新的转换结果。50是超时时间(毫秒)。- 映射计算:这是核心算法。将ADC的原始值(0-4095)线性映射到舵机控制所需的脉冲计数值(5-25)。这个映射关系决定了电位器旋转角度与舵机转动角度的线性对应关系。
5:对应0.5ms脉冲,舵机0°位置。25:对应2.5ms脉冲,舵机180°位置。- 公式
servo_pulse = 5 + (adc_value * 20) / 4095;实现了线性映射。
__HAL_TIM_SET_COMPARE:这个宏是HAL库提供的,用于设置定时器捕获/比较寄存器(CCR)的值。更新CCR值会立即改变下一个PWM周期的脉冲宽度,从而控制舵机角度。HAL_Delay:添加一个短延时,可以降低CPU使用率,并让舵机有足够时间响应。对于舵机,10-20ms的更新周期是合适的。
7. 功能测试与效果验证
代码编写完成后,编译、下载到开发板,即可进行测试。
测试步骤:
- 编译与下载:在Keil或CubeIDE中点击编译按钮(通常是一个锤子图标),确保0错误,0警告。然后点击下载/调试按钮,将程序烧录到STM32。
- 硬件上电:给开发板上电(通过USB或外部电源),并为舵机供电。
- 观察现象:
- 初始状态:舵机应该转动到一个初始角度(对应ADC初始值,可能是中间位置)。
- 旋转电位器:缓慢旋转电位器。你应该能看到舵机跟随电位器的旋转而平滑转动。
- 极限位置测试:将电位器拧到一端(接近GND),舵机应转到0°附近(最小角度)。拧到另一端(接近3.3V),舵机应转到180°附近(最大角度)。
- 调试与优化:
- 舵机抖动:如果舵机在某个位置轻微抖动或不稳,可能是电源噪声或PWM信号不干净。可以尝试在舵机电源端并联一个
100uF的电解电容进行滤波。同时检查代码中映射计算是否有误,导致PWM脉宽在边界值附近跳动。 - 角度不线性:如果感觉舵机转动与电位器旋转不是良好的线性关系,首先检查电位器本身的质量和线性度。其次,可以尝试在代码中增加映射校准。例如,先读取电位器两端的ADC值(
adc_min,adc_max),再进行映射,以消除硬件误差。 - 使用串口调试:如果现象不符合预期,强烈建议将ADC值和计算出的
servo_pulse值通过串口打印出来。这能帮你确认是ADC采样问题,还是PWM计算问题。你需要先配置一个USART,并重写fputc函数以重定向printf。
- 舵机抖动:如果舵机在某个位置轻微抖动或不稳,可能是电源噪声或PWM信号不干净。可以尝试在舵机电源端并联一个
串口打印调试代码示例(补充):在main.c中/* USER CODE BEGIN 4 */和/* USER CODE END 4 */之间添加:
#ifdef __GNUC__ #define PUTCHAR_PROTOTYPE int __io_putchar(int ch) #else #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f) #endif PUTCHAR_PROTOTYPE { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); // 假设使用USART1 return ch; }并在while循环中取消注释或添加printf语句。在电脑上打开串口助手,设置正确的波特率(如115200),即可看到实时数据。
8. 进阶优化与扩展思路
基础功能实现后,可以考虑以下优化和扩展,让项目更健壮、更实用。
1. 使用DMA传输ADC数据轮询方式占用CPU。对于多通道ADC采样或需要高频采样的场景,使用DMA(直接存储器访问)是更好的选择。可以在CubeMX中配置ADC为“DMA Continuous Requests”,并设置一个内存缓冲区。ADC转换完成后自动通过DMA将数据搬运到缓冲区,CPU只需读取缓冲区数据即可,效率极高。
2. 增加软件滤波电位器信号可能存在抖动,导致ADC值微小波动,进而引起舵机轻微震颤。可以增加简单的软件滤波,如移动平均滤波或中值滤波。
// 示例:简单的移动平均滤波(队列长度为5) #define FILTER_LEN 5 uint32_t adc_buffer[FILTER_LEN] = {0}; uint8_t buffer_index = 0; uint32_t adc_filtered = 0; // 在主循环中 adc_buffer[buffer_index] = HAL_ADC_GetValue(&hadc1); buffer_index = (buffer_index + 1) % FILTER_LEN; adc_filtered = 0; for(int i=0; i<FILTER_LEN; i++) { adc_filtered += adc_buffer[i]; } adc_filtered /= FILTER_LEN; // 使用 adc_filtered 进行后续映射计算3. 实现非线性映射或角度预设不是所有应用都需要线性控制。你可以修改映射算法,实现例如“电位器在中间区域变化慢,两端变化快”的非线性响应,或者设置几个固定的角度档位(通过按键切换),电位器用于微调。
4. 控制多个舵机利用STM32一个定时器的多个通道,可以同时产生多路PWM。例如,TIM1有4个通道,可以同时控制4个舵机。你需要配置多个ADC通道(或使用一个ADC分时复用)来读取多个电位器的值,并分别更新对应的TIM通道CCR值。
5. 加入PID控制如果舵机带负载,并且希望它快速、稳定、无超调地到达指定位置,可以引入PID控制算法。将ADC值作为目标角度,通过编码器或其他传感器读取舵机实际角度作为反馈,计算PID输出并调整PWM。这将项目从开环控制升级为闭环控制。
9. 常见问题与排查方法
在实践过程中,你可能会遇到以下问题,这里提供排查思路。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 舵机完全不转动 | 1. 电源问题(电压不足或电流不够) 2. 信号线未连接或接触不良 3. PWM信号未产生或频率不对 4. 代码未成功下载 | 1. 用万用表测量舵机VCC和GND间电压。 2. 检查信号线连接引脚是否与代码一致。 3. 用示波器或逻辑分析仪检查PWM引脚波形。 4. 检查开发板指示灯,尝试烧录一个简单的LED闪烁程序。 | 1. 确保供电电压在舵机工作范围内(如5V),并提供足够电流(外接电源)。 2. 重新插拔连接线,确认引脚定义。 3. 检查CubeMX中TIM配置,特别是PSC和ARR计算,确保PWM频率为50Hz。 4. 检查下载器连接、驱动、芯片型号选择。 |
| 舵机只在一个方向转动到底,不回转 | PWM脉冲宽度范围不正确,始终输出最大或最小脉宽。 | 通过串口打印adc_value和servo_pulse的值。旋转电位器,观察数值变化。 | 检查映射计算公式。确认ADC值范围是否正确(0-4095),确认目标脉冲计数值范围(如5-25)是否正确。 |
| 舵机转动不顺畅,有抖动 | 1. 电源噪声或功率不足。 2. 电位器接触不良或ADC采样值跳动。 3. 代码更新PWM过于频繁或计算有误。 | 1. 在舵机电源端并联大电容(如100uF电解电容+0.1uF陶瓷电容)。 2. 通过串口观察ADC原始值是否稳定。 3. 检查 HAL_Delay的延时时间是否太短。 | 1. 加强电源滤波,使用独立电源为舵机供电。 2. 更换电位器,或在代码中加入软件滤波(见上一节)。 3. 适当增加主循环中的延时(如20-50ms)。 |
| ADC采样值不变化或变化范围小 | 1. 电位器接错线(中间引脚未接ADC)。 2. ADC通道配置错误。 3. 参考电压问题(VDDA、VSSA)。 | 1. 用万用表测量电位器中间引脚电压是否随旋转变化。 2. 检查CubeMX中ADC通道与引脚对应关系。 3. 对于F1系列,确保VDDA和VSSA正确连接(通常与VDD和VSS相连)。 | 1. 纠正电位器接线。 2. 在CubeMX中核对引脚配置,重新生成代码。 3. 确保VDDA接到3.3V,VSSA接到GND。 |
| 代码编译通过,但下载后无任何反应 | 1. 系统时钟配置错误,导致外设时钟不对。 2. 未启动ADC或PWM。 3. 程序跑飞或进入错误中断。 | 1. 检查CubeMX时钟树配置,确认系统时钟和外设时钟频率正确。 2. 检查 main.c中是否调用了HAL_ADC_Start和HAL_TIM_PWM_Start。3. 在 while(1)开始处加一个LED翻转代码,看程序是否运行。 | 1. 在CubeMX中仔细检查HSE、PLL、系统时钟源的选择和分频设置。 2. 确保启动代码被正确执行。 3. 检查是否有未处理的硬件错误,可以重写 HardFault_Handler函数进行调试。 |
10. 项目总结与下一步
通过这个“STM32电位器PWM控制舵机”的项目,我们完整地实践了从硬件连接到软件配置、从外设驱动到应用逻辑的嵌入式开发流程。核心收获在于掌握了ADC采样模拟信号和TIM产生PWM信号这两项嵌入式系统中最常用的技能,并理解了如何将两者结合起来实现一个简单的闭环(人机交互)控制。
这个项目是一个强大的起点。你可以基于它进行多种扩展:
- 多自由度控制:使用多个电位器和多个舵机,制作一个多关节的机械臂控制器。
- 无线控制:将电位器替换为蓝牙(HC-05/06)或Wi-Fi(ESP8266)模块接收的手机指令,实现无线遥控。
- 位置反馈:为舵机加装电位器或编码器作为位置传感器,实现真正的闭环PID位置控制,精度和抗干扰能力会大幅提升。
- 集成到更大系统:将此功能作为一个模块,嵌入到智能小车、四足机器人或自动浇花系统中。
建议你在成功实现基础功能后,务必尝试使用串口调试和软件滤波这两个调试与优化技巧,它们能极大提升你解决实际问题的能力。最后,记得整理好你的工程代码和配置笔记,这个项目模板在未来很多需要模拟量输入和电机控制的场景中都能快速复用。
