1. 项目概述
在嵌入式开发领域,舵机控制是一个基础但极其重要的应用场景。无论是机器人关节控制、无人机舵面调节,还是智能小车的转向系统,都离不开精准的舵机控制。而STM32作为嵌入式开发的主流平台,其标准库和HAL库的开发方式各有优劣,选择哪种方式往往让初学者感到困惑。
我最近在开发一个机械臂项目时,就遇到了这个问题。项目需要控制6个舵机协同工作,对PWM信号的精度和稳定性要求很高。经过反复实践和对比测试,我总结出了一套完整的解决方案,今天就来分享STM32标准库和HAL库在舵机控制上的实现差异和实战经验。
2. 实验准备
2.1 硬件清单
在开始之前,我们需要准备以下硬件设备:
-
STM32F103C8T6最小系统板:这是我们的控制核心,选择这款开发板是因为它性价比高,资源丰富,特别适合学习和中小型项目开发。板载的STM32F103C8T6芯片有3个通用定时器,可以满足多路PWM输出需求。
-
SG90舵机:这是最常用的9g微型舵机,工作电压4.8-6V,扭矩1.6kg/cm。它的控制信号是标准的50Hz PWM波,非常适合用来学习舵机控制原理。
-
USB-TTL模块:用于程序下载和串口调试。我推荐使用CH340G芯片的模块,稳定性好,驱动安装简单。
-
5V电源模块:这一点非常重要!很多初学者直接用开发板的5V给舵机供电,结果发现舵机要么不转,要么抖动严重。这是因为舵机在启动瞬间电流可能达到500mA以上,开发板的稳压芯片无法提供这么大的电流。建议使用外接的5V 2A电源单独给舵机供电。
-
杜邦线:准备公对公、公对母各若干,用于连接开发板和外围设备。
2.2 软件环境
软件方面需要准备以下工具:
-
Keil MDK-ARM V5.38:这是STM32开发的主流IDE,提供了完善的编译、调试功能。安装时记得同时安装对应的设备支持包。
-
STM32标准库:版本选择STM32F10x_StdPeriph_Lib_V3.5.0。这个库提供了对STM32F1系列芯片寄存器的封装,可以大大简化开发流程。
-
STM32CubeMX:版本6.9.0,配合STM32F1xx HAL库使用。CubeMX的图形化配置界面可以自动生成初始化代码,显著提高开发效率。
-
串口调试助手:我习惯用SSCOM V5.13.1,界面简洁,功能齐全。当然你也可以选择其他喜欢的串口工具。
2.3 舵机控制原理
理解舵机的工作原理对后续开发至关重要。SG90这类标准舵机的控制信号是频率为50Hz(周期20ms)的PWM波,通过调节高电平的脉宽来控制舵机角度:
- 0.5ms高电平对应0°位置
- 1.5ms高电平对应90°位置
- 2.5ms高电平对应180°位置
这里有个重要的细节:不同品牌的舵机,脉宽和角度的对应关系可能略有差异。比如有些舵机0°对应的是1ms脉宽,180°对应2ms脉宽。实际项目中需要根据具体舵机的规格书进行调整。
在STM32上,我们通常使用定时器的PWM模式来生成这种信号。以TIM3为例,配置步骤如下:
- 设置定时器时钟为1MHz(72MHz主频/72分频)
- 设置自动重装载值为19999,这样定时器周期就是20000个时钟周期,即20ms
- 配置PWM模式,通过改变比较寄存器的值来调整高电平脉宽
3. STM32标准库实现舵机控制
3.1 工程搭建步骤
3.1.1 新建Keil工程
打开Keil MDK,点击Project->New μVision Project,命名为"STM32F103_StdLib_Servo"。芯片选择STM32F103C8,这是我们的目标MCU。
这里有个小技巧:在弹出"Select Device for Target"窗口时,选择"否"不添加启动文件。因为标准库已经包含了启动文件,我们稍后手动添加更合适。
3.1.2 添加标准库文件
标准库工程需要手动组织文件结构,我建议按以下方式组织:
code复制Project
├── CORE
│ ├── startup_stm32f10x_md.s
│ └── stm32f10x.h
├── FWLIB
│ ├── src
│ └── inc
├── USER
│ ├── main.c
│ └── stm32f10x_conf.h
关键点说明:
- startup_stm32f10x_md.s是启动文件,"md"表示中等容量设备(STM32F103C8属于这个类别)
- stm32f10x.h包含所有外设寄存器的定义
- FWLIB/src下添加需要用到的外设库文件,如stm32f10x_gpio.c、stm32f10x_tim.c等
- stm32f10x_conf.h用于配置使用哪些外设,可以减小代码体积
3.1.3 配置工程参数
点击"魔法棒"图标进入配置:
- Target选项卡:设置晶振频率为8MHz(根据实际硬件),Stack Size设为0x200,Heap Size设为0x100
- Output选项卡:勾选Create HEX File,方便后续烧录
- C/C++选项卡:在Define中添加USE_STDPERIPH_DRIVER和STM32F10X_MD
- Include Paths:添加CORE、FWLIB/inc、USER等路径
3.2 硬件连接说明
将STM32F103C8T6与SG90舵机连接如下:
| STM32引脚 | 舵机接口 | 说明 |
|---|---|---|
| PB0 | 信号线(黄) | TIM3_CH3输出 |
| 5V | 电源线(红) | 外接5V电源 |
| GND | 地线(棕) | 共地 |
特别注意:舵机电源一定要外接!开发板的5V输出电流有限,直接供电会导致舵机工作不稳定。
3.3 代码编写
3.3.1 主程序(main.c)
主程序主要完成以下功能:
- 系统时钟配置:将主频设置为72MHz
- GPIO初始化:配置PB0为TIM3_CH3复用输出
- 定时器配置:设置TIM3为PWM模式,周期20ms
- 角度控制函数:将角度转换为对应的PWM脉宽
c复制#include "stm32f10x.h"
#define SERVO_TIM TIM3
#define SERVO_CHANNEL TIM_Channel_3
#define SERVO_GPIO_PORT GPIOB
#define SERVO_GPIO_PIN GPIO_Pin_0
void RCC_Configuration(void);
void GPIO_Configuration(void);
void TIM_Configuration(void);
void Servo_SetAngle(u16 angle);
void RCC_Configuration(void) {
// 启用外部高速时钟HSE
RCC_HSEConfig(RCC_HSE_ON);
while(RCC_GetFlagStatus(RCC_FLAG_HSERDY) == RESET);
// 配置PLL:HSE输入,9倍频,输出72MHz
RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9);
RCC_PLLCmd(ENABLE);
while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);
// 设置时钟分频
RCC_HCLKConfig(RCC_SYSCLK_Div1); // AHB 72MHz
RCC_PCLK1Config(RCC_HCLK_Div2); // APB1 36MHz
RCC_PCLK2Config(RCC_HCLK_Div1); // APB2 72MHz
// 切换系统时钟到PLL
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
while(RCC_GetSYSCLKSource() != 0x08);
}
void GPIO_Configuration(void) {
GPIO_InitTypeDef GPIO_InitStructure;
// 使能GPIOB和TIM3时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
// 配置PB0为复用推挽输出
GPIO_InitStructure.GPIO_Pin = SERVO_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SERVO_GPIO_PORT, &GPIO_InitStructure);
}
void TIM_Configuration(void) {
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
// 定时器基础配置
TIM_TimeBaseStructure.TIM_Prescaler = 71; // 72MHz/72=1MHz
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_Period = 19999; // 20000个计数=20ms
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(SERVO_TIM, &TIM_TimeBaseStructure);
// PWM模式配置
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 750; // 初始1.5ms(90°)
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
// 配置TIM3通道3
TIM_OC3Init(SERVO_TIM, &TIM_OCInitStructure);
TIM_OC3PreloadConfig(SERVO_TIM, TIM_OCPreload_Enable);
TIM_ARRPreloadConfig(SERVO_TIM, ENABLE);
TIM_Cmd(SERVO_TIM, ENABLE);
}
void Servo_SetAngle(u16 angle) {
u16 pulse;
// 角度限制在0-180°
angle = angle > 180 ? 180 : angle;
angle = angle < 0 ? 0 : angle;
// 计算脉宽:500(0°)-2500(180°)
pulse = 500 + (angle * 100 / 9);
// 更新比较值
TIM_SetCompare3(SERVO_TIM, pulse);
}
void Delay_ms(u16 ms) {
u16 i,j;
for(i=0; i<ms; i++)
for(j=0; j<1141; j++);
}
int main(void) {
RCC_Configuration();
GPIO_Configuration();
TIM_Configuration();
while(1) {
// 0°→180°扫描
for(u16 angle=0; angle<=180; angle+=10) {
Servo_SetAngle(angle);
Delay_ms(1000);
}
// 180°→0°扫描
for(u16 angle=180; angle>=0; angle-=10) {
Servo_SetAngle(angle);
Delay_ms(1000);
}
}
}
3.3.2 配置文件(stm32f10x_conf.h)
这个文件用于管理使用哪些外设,避免编译未使用的库代码:
c复制#ifndef __STM32F10x_CONF_H
#define __STM32F10x_CONF_H
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_tim.h"
#define USE_STDPERIPH_DRIVER
#define STM32F10X_MD
#endif
3.4 代码烧录与调试
完成代码编写后,按以下步骤烧录和调试:
- 使用ST-Link连接开发板,在Keil中点击Download按钮烧录程序
- 连接舵机,确保电源接线正确
- 上电后观察舵机是否按预期0°→180°往复运动
常见问题排查:
- 舵机无反应:检查电源是否接通,信号线是否接对
- 舵机抖动:检查电源功率是否足够,PWM信号是否稳定
- 角度不准:调整Servo_SetAngle函数中的脉宽计算公式
4. STM32 HAL库实现舵机控制
4.1 工程搭建步骤
4.1.1 新建CubeMX工程
打开STM32CubeMX,点击New Project,搜索并选择STM32F103C8Tx芯片。在Project Manager中设置工程名称为"STM32F103_HAL_Servo",工具链选择MDK-ARM。
这里有个实用技巧:在Code Generator选项卡中,勾选"Generate peripheral initialization as a pair of '.c/.h' files per peripheral"。这样CubeMX会为每个外设生成独立的初始化文件,方便管理。
4.1.2 外设配置
-
时钟配置:
- 选择HSE作为时钟源
- 设置PLL倍频为9,系统时钟72MHz
- APB1 Prescaler设为2(36MHz),APB2 Prescaler设为1(72MHz)
-
定时器配置:
- 启用TIM3,选择Channel3的PWM Generation CH3模式
- Prescaler设为71,Counter Period设为19999
- PWM Mode1,Pulse初始值750,Polarity High
-
GPIO配置:
- PB0会自动配置为TIM3_CH3,无需手动修改
- 建议将GPIO速度设置为High,确保信号质量
4.1.3 生成代码
点击Generate Code生成工程,然后点击Open Project在Keil中打开。CubeMX会自动生成所有初始化代码,我们只需要在main.c中添加业务逻辑。
4.2 硬件连接
硬件连接与标准库版本完全相同:
| STM32引脚 | 舵机接口 | 说明 |
|---|---|---|
| PB0 | 信号线(黄) | TIM3_CH3输出 |
| 5V | 电源线(红) | 外接5V电源 |
| GND | 地线(棕) | 共地 |
4.3 代码编写
4.3.1 主程序(main.c)
HAL库版本的主程序要简洁很多,因为大部分初始化工作已经由CubeMX完成:
c复制#include "main.h"
#include "tim.h"
#include "gpio.h"
void Servo_SetAngle(uint16_t angle) {
uint16_t pulse;
// 角度限制
angle = angle > 180 ? 180 : angle;
angle = angle < 0 ? 0 : angle;
// 计算脉宽
pulse = 500 + (angle * 100 / 9);
// 更新比较值
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, pulse);
}
void Delay_ms(uint32_t ms) {
uint32_t i,j;
for(i=0; i<ms; i++)
for(j=0; j<1141; j++);
}
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_TIM3_Init();
// 启动PWM输出
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3);
while (1) {
// 0°→180°扫描
for(uint16_t angle=0; angle<=180; angle+=10) {
Servo_SetAngle(angle);
Delay_ms(1000);
}
// 180°→0°扫描
for(uint16_t angle=180; angle>=0; angle-=10) {
Servo_SetAngle(angle);
Delay_ms(1000);
}
}
}
4.3.2 定时器配置(tim.c)
CubeMX生成的定时器初始化代码非常完整,包含了所有必要的配置:
c复制void MX_TIM3_Init(void) {
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
TIM_OC_InitTypeDef sConfigOC = {0};
htim3.Instance = TIM3;
htim3.Init.Prescaler = 71;
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 19999;
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
if (HAL_TIM_Base_Init(&htim3) != HAL_OK) {
Error_Handler();
}
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
if (HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig) != HAL_OK) {
Error_Handler();
}
if (HAL_TIM_PWM_Init(&htim3) != HAL_OK) {
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig) != HAL_OK) {
Error_Handler();
}
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 750;
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
if (HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_3) != HAL_OK) {
Error_Handler();
}
HAL_TIM_MspPostInit(&htim3);
}
4.4 代码烧录与调试
烧录步骤与标准库版本相同:
- 使用ST-Link连接开发板
- 在Keil中点击Download烧录程序
- 上电观察舵机运动状态
HAL库版本特有的调试技巧:
- 如果舵机不转,检查HAL_TIM_PWM_Start是否调用
- 使用HAL库的调试功能,可以在Error_Handler中设置断点,方便定位问题
- HAL库提供了更完善的错误处理机制,可以利用这个特点增强程序健壮性
5. 标准库与HAL库实现流程对比
5.1 开发流程差异
标准库开发流程:
- 手动创建Keil工程
- 添加标准库文件
- 编写寄存器配置代码
- 实现业务逻辑
- 烧录调试
HAL库开发流程:
- 使用CubeMX创建工程
- 图形化配置时钟和外设
- 生成初始化代码
- 添加业务逻辑
- 烧录调试
5.2 核心差异对比
| 对比维度 | 标准库 | HAL库 |
|---|---|---|
| 开发方式 | 手动编写寄存器配置 | 图形化配置+自动生成代码 |
| 代码量 | 较多,需要编写大量初始化代码 | 较少,只需关注业务逻辑 |
| 学习曲线 | 较陡峭,需要理解寄存器原理 | 较平缓,抽象层次更高 |
| 执行效率 | 更高,直接操作寄存器 | 略低,有额外的函数调用开销 |
| 移植性 | 较差,不同系列需要重写代码 | 较好,同一套代码可适配不同系列 |
| 调试便��性 | 需要熟悉寄存器手册 | 有完善的错误处理机制 |
5.3 选择建议
根据项目需求选择合适的开发方式:
-
选择标准库的情况:
- 对性能要求极高
- 需要精确控制硬件
- 项目规模小,不需要频繁移植
- 开发者熟悉STM32��存器
-
选择HAL库的情况:
- 快速原型开发
- 需要跨系列兼容
- 团队协作项目
- 初学者学习
6. 常见问题与解决方案
6.1 舵机抖动严重
可能原因及解决方案:
-
电源问题:
- 现象:舵机在运动时出现不规则抖动
- 解决方案:使用外接5V 2A以上电源单独给舵机供电,确保电源线足够粗(建议18AWG以上)
-
PWM信号不稳定:
- 现象:舵机在固定角度时仍有微小抖动
- 解决方案:检查定时器配置,确保PWM频率严格为50Hz;使用示波器观察信号波形
-
机械负载过大:
- 现象:带载时抖动明显
- 解决方案:选择扭矩更大的舵机,或减轻负载
6.2 舵机角度与理论值偏差
校准方法:
-
使用标准角度测试:
- 发送1.5ms脉宽(90°),观察实际角度
- 根据偏差调整脉宽计算公式
-
修改Servo_SetAngle函数:
c复制// 原始公式 pulse = 500 + (angle * 100 / 9); // 校准后公式示例 pulse = 480 + (angle * 102 / 9); // 根据实测调整系数 -
创建校准表:
对于高精度应用,可以建立角度-脉宽的查找表,实现非线性校准
6.3 HAL库编译报错
常见错误及解决:
-
未定义标识符错误:
- 现象:htim3等变量未定义
- 解决:检查是否包含了对应的头文件(如tim.h)
-
链接错误:
- 现象:缺少HAL库函数
- 解决:在CubeMX中确认已生成所有必要的外设初始化代码
-
硬件抽象层错误:
- 现象:HAL_Init()失败
- 解决:检查系统时钟配置,确保晶振设置正确
7. 进阶优化建议
7.1 精准延时优化
替换软件延时为硬件定时器中断,提高时间精度:
- 配置一个基本定时器(如TIM6)
- 设置适当的中断周期(如1ms)
- 在中断服务函数中实现精确计时
c复制// 在HAL库中实现
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if(htim->Instance == TIM6) {
uwTick++;
}
}
7.2 多舵机控制
利用STM32的多个定时器通道控制多个舵机:
- 配置TIM3的多个通道(CH1-CH4)
- 为每个舵机编写独立的角度控制函数
- 同步更新多个通道的比较值
c复制// 控制两个舵机示例
void Servo_SetAngles(uint16_t angle1, uint16_t angle2) {
uint16_t pulse1 = 500 + (angle1 * 100 / 9);
uint16_t pulse2 = 500 + (angle2 * 100 / 9);
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pulse1);
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, pulse2);
}
7.3 串口控制接口
添加串口通信功能,实现远程控制:
- 配置USART外设
- 实现简单的通信协议
- 解析指令并设置舵机角度
c复制void USART1_IRQHandler(void) {
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE)) {
uint8_t ch = (uint8_t)(huart1.Instance->DR & 0xFF);
// 解析指令并调用Servo_SetAngle
}
}
7.4 闭环控制实现
增加电位器或编码器反馈,实现闭环控制:
- 配置ADC读取电位器电压
- 计算当前角度
- 使用PID算法调整PWM输出
c复制// 简单的PID控制示例
float PID_Control(float target, float current) {
static float integral = 0;
static float last_error = 0;
float error = target - current;
integral += error;
float derivative = error - last_error;
last_error = error;
return Kp*error + Ki*integral + Kd*derivative;
}
8. 项目总结与个人心得
经过标准库和HAL库两种方式的实践,我对STM32的舵机控制有了更深入的理解。标准库虽然需要编写更多的底层代码,但执行效率高,适合对性能要求严格的场景。而HAL库通过CubeMX的图形化配置,大大提高了开发效率,特别适合快速原型开发。
在实际项目中,我有几点深刻体会:
-
电源质量至关重要:舵机对电源非常敏感,一定要使用足够功率的电源,并做好滤波。我曾遇到过一个诡异的抖动问题,折腾了很久才发现是电源线太细导致的压降。
-
信号稳定性:PWM信号的稳定性直接影响舵机性能。长距离传输时,建议使用屏蔽线,或者在信号线上加个小电容滤波。
-
机械结构影响:舵机的实际表现很大程度上受机械结构影响。装配时要确保舵机安装牢固,传动部件没有过大的间隙。
-
温度影响:长时间工作后,舵机内部温度升高会导致性能变化。高精度应用中需要考虑温度补偿。
对于初学者,我建议从HAL库入手,快速实现功能,等熟悉后再深入研究标准库。而对于经验丰富的开发者,可以根据项目需求灵活选择,甚至混合使用两种方式。
