文章前言

本文基于 STM32F103C8T6 最小系统板,搭配经典 SG90 微型舵机、0.96 寸 I2C OLED 屏幕,完整讲解硬件接线、舵机底层控制原理、定时器 PWM 配置逻辑,附带可直接编译运行的工程源码,搭配实物演示效果,零基础也能跟着复现。

工程实现功能:

  1. 利用 TIM4_CH1 (PB6) 输出 50Hz 标准舵机 PWM 信号,控制 SG90 0~180° 精准转动;
  2. 角度与 PWM 脉宽线性换算,内部整数运算无浮点,运行效率更高;
  3. OLED 实时打印 PWM 频率、周期、当前设定角度、输出脉宽;
  4. 上电自动循环角度演示:0°→45°→90°→135°→180° 往复切换,每个角度停留 1 秒;
  5. 封装完整舵机驱动函数,支持直接设置角度、手动输入脉宽、读取当前参数、停止 PWM 输出。

视频讲解:STM32F103驱动SG90舵机+OLED显示全套教程(原理 + 接线 + 源码 + 实物演示)https://www.bilibili.com/video/BV1H5gp6tExt/?share_source=copy_web&vd_source=91b967b41a17a5686913521545a8c3fb

一、整体工作流程概述

整套设备的控制逻辑非常清晰,完整执行流程如下:

  1. 程序输入 0~180° 目标角度,通过公式换算成 500~2500μs 高电平脉宽;
  2. STM32 定时器 TIM4 配置 20ms 周期(50Hz)PWM,PB6 输出对应宽度脉冲;
  3. 脉冲信号送入 SG90 舵机信号引脚,舵机内部闭环电路解析目标位置;
  4. 舵机自带电位器实时采集实际转轴角度,对比目标值自动驱动电机修正位置;
  5. 转轴到达目标角度后电机停转,同时 OLED 刷新当前全部控制参数。

简单概括:STM32 输出不同宽度脉冲给舵机下达角度指令,舵机依靠内部反馈自行完成定位,屏幕实时展示控制参数。

二、硬件接线与供电说明

2.1 模块引脚对应表

外设模块引脚STM32F103 引脚功能说明
SG90 橙色信号线 (Signal)PB6TIM4_CH1 PWM 控制输出
SG90 红色电源线 (VCC)外部独立 5V 电源正极舵机工作供电
SG90 棕色地线 (GND)STM32 公共 GND共地参考电平
OLED SCL 时钟PB8软件 I2C 时钟
OLED SDA 数据PB9软件 I2C 数据
OLED VCC开发板 3.3V屏幕供电
OLED GNDSTM32 公共 GND屏幕接地

2.2 推荐供电接线拓扑

外部 5V 电源正极 → SG90 红线 外部 5V 电源负极分两路:一路接 SG90 棕线,一路接 STM32 所有 GND STM32 PB6 引脚 → SG90 橙色信号线

这个位置需要注意一下:舵机的负极,可能是黑色的线,也可能是棕色的线。

2.3 两个关键接线注意事项

  1. 必须共地 STM32 输出高低电平全部以自身 GND 为参考基准,若舵机电源地和单片机地断开,舵机无法识别脉冲信号,会出现完全不转、剧烈抖动、角度乱跑故障。哪怕舵机使用独立电源,地线一定要相连。
  2. 舵机禁止单片机 GPIO 供电,建议独立 5V 供电 SG90 启动、换向、受阻时瞬时电流很大,直接用开发板板载 3.3V/5V 供电会引发一系列问题:
  • 单片机电压骤降,频繁自动复位;
  • OLED 屏幕闪烁、文字显示错乱;
  • 舵机转动无力、持续抖动;
  • 开发板稳压芯片过热。 解决方案:外接稳定 5V 电源给舵机供电,电源输入端并联滤波电容,减少电流冲击干扰。

三、SG90 舵机内部控制原理

SG90 属于位置闭环式微型舵机,内部集成四大核心部件:小型直流电机、减速齿轮组、角度反馈电位器、专用驱动解码电路,无需单片机读取转轴位置,自带闭环调节。

完整工作逻辑:

  1. 舵机内部电路持续测量输入 PWM 高电平时长,解析出程序设定的目标角度;
  2. 电位器跟随舵盘同步旋转,输出对应实际物理角度的反馈电压;
  3. 驱动电路对比目标角度电压和实际反馈电压,计算位置误差;
  4. 存在误差时自动控制电机正转 / 反转,经过齿轮减速放大扭矩带动舵盘;
  5. 转轴匹配目标角度、误差趋近于 0 时电机断电停转;
  6. 若外力掰动舵盘偏离设定位置,闭环系统会立刻重新驱动电机复位。

核心优势:单片机只需要下发角度指令,不需要编写位置采集、PID 调节代码,舵机自主完成定位控制。

四、舵机标准 PWM 控制信号详解

SG90 行业通用控制标准:固定周期 20ms,对应频率 50Hz,转轴角度仅由高电平脉宽决定,和普通调速 PWM 区分开。 一个完整周期波形规则:20ms 内,0.5ms~2.5ms 高电平,剩余时长全部为低电平。

4.1 角度 - 脉宽对应参数表

目标角度高电平脉宽20ms 周期占空比
500us2.5%
45°1000us5.0%
90°(中位)1500us7.5%
135°2000us10.0%
180°2500us12.5%

4.2 角度与脉宽换算公式

角度转脉宽(程序核心换算逻辑): 脉宽 (us) = 500 + 角度 × 2000 ÷ 180

脉宽反向计算对应角度: 角度 = (脉宽 - 500) × 180 ÷ 2000

4.3 使用提醒

不同厂商 SG90 机械限位存在个体差异,500~2500us 为通用标准区间。禁止脉宽超出该范围,舵机堵转后会大电流发热,严重磨损内部齿轮。

五、STM32 TIM4 定时器 PWM 生成原理

本工程系统主时钟 72MHz,PB6 复用功能为 TIM4_CH1,定时器配置参数如下:

  1. 定时器原始时钟:72MHz
  2. 预分频 PSC = 71(分频 72),计数频率 = 72MHz / 72 = 1MHz
  3. 单次计数单位:1us
  4. 自动重装载值 ARR = 19999,完整周期 = 20000 × 1us = 20ms
  5. PWM 输出频率 = 1 ÷ 0.02s = 50Hz

通道配置为 PWM1 模式,计数规则: 计数器数值 < CCR1(比较寄存器)时,PB6 输出高电平; 计数器数值 ≥ CCR1 时,PB6 输出低电平。

设计亮点:计数单位刚好 1 微秒,CCR 寄存器写入数值直接等于高电平脉宽,换算逻辑极简,新手极易理解。

CCR1 寄存器值PB6 高电平时长对应舵机角度
500500us
15001500us90°
25002500us180°

六、源代码工程模块说明

6.1 工程文件分工

下文完整源码分为 OLED 驱动、SG90 舵机驱动、主函数三大部分,可直接复制到 Keil 工程使用。

文件核心函数功能作用
SG90.cSG90_Init()初始化 PB6、TIM4,上电舵机默认中位 90°
SG90.cSG90_SetAngle()输入 0~180 角度,自动换算脉宽更新 CCR1
SG90.cSG90_SetPulseWidth()手动自定义 500~2500us 脉宽,用于舵机标定
SG90.cSG90_Stop()清空 CCR1,停止输出有效舵机脉冲
SG90.cSG90_GetAngle() / SG90_GetPulseWidth()读取当前设定参数,供 OLED 显示调用
main.cSG90_ShowState()在 OLED 屏幕打印全部 PWM、角度参数

main.c代码

/* SG90 servo teaching demonstration for STM32F103C8T6. */
#include "delay.h"
#include "sys.h"
#include "oled.h"
#include "SG90.h"

#define DEMO_HOLD_TIME_MS  1000U

static void SG90_ShowState(uint16_t angle);

int main(void)
{
	static const uint16_t demo_angles[] =
	{
		0U, 45U, 90U, 135U, 180U, 135U, 90U, 45U
	};
	uint8_t index = 0U;

	delay_init();
	NVIC_Configuration();
	SG90_Init();
	OLED_Init();
	OLED_ColorTurn(0);
	OLED_DisplayTurn(0);
	SG90_ShowState(SG90_GetAngle());
	delay_ms(DEMO_HOLD_TIME_MS);

	while(1)
	{
		SG90_SetAngle(demo_angles[index]);
		SG90_ShowState(demo_angles[index]);

		index++;
		if(index >= (sizeof(demo_angles) / sizeof(demo_angles[0])))
		{
			index = 0U;
		}

		delay_ms(DEMO_HOLD_TIME_MS);
	}
}

static void SG90_ShowState(uint16_t angle)
{
	OLED_Clear();
	OLED_ShowString(0, 0, (u8 *)"SG90 SERVO DEMO", 12);
	OLED_ShowString(0, 12, (u8 *)"PB6 / TIM4_CH1", 12);
	OLED_ShowString(0, 24, (u8 *)"PWM:50Hz T:20ms", 12);
	OLED_ShowString(0, 38, (u8 *)"Angle:", 12);
	OLED_ShowNum(42, 36, angle, 3, 16);
	OLED_ShowString(70, 38, (u8 *)"deg", 12);
	OLED_ShowString(0, 52, (u8 *)"Pulse:", 12);
	OLED_ShowNum(42, 52, SG90_GetPulseWidth(), 4, 12);
	OLED_ShowString(70, 52, (u8 *)"us", 12);
	OLED_Refresh();
}

SG90.c代码

#include "SG90.h"
#include "stm32f10x_tim.h"

/* 保存最近一次设置的角度和脉宽,供 OLED 显示或程序查询。 */
static uint16_t sg90_angle = 90U;
static uint16_t sg90_pulse_us = SG90_PULSE_CENTER_US;

/*
 * SG90 PWM 输出初始化:
 * 1. STM32F103C8T6 默认引脚映射中,PB6 对应 TIM4_CH1。
 * 2. 本工程系统时钟为 72 MHz,TIM4 的计数时钟同样为 72 MHz。
 * 3. 预分频器设置为 72-1,使定时器每 1 us 计数一次。
 * 4. 自动重装载值设置为 20000-1,得到 20 ms(50 Hz)的 PWM 周期。
 * 5. 初始脉宽为 1500 us,对应舵机中间位置 90°。
 */
void SG90_Init(void)
{
	GPIO_InitTypeDef gpio_init;
	TIM_TimeBaseInitTypeDef timer_init;
	TIM_OCInitTypeDef pwm_init;

	/* 开启 GPIOB 和 TIM4 外设时钟。 */
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);

	/* PB6 配置为复用推挽输出,由 TIM4_CH1 控制引脚电平。 */
	gpio_init.GPIO_Pin = GPIO_Pin_6;
	gpio_init.GPIO_Mode = GPIO_Mode_AF_PP;
	gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &gpio_init);

	/* TIM4 使用内部时钟。 */
	TIM_InternalClockConfig(TIM4);

	/* 计数分辨率为 1 us,PWM 周期为 20000 us。 */
	timer_init.TIM_Prescaler = 72U - 1U;
	timer_init.TIM_CounterMode = TIM_CounterMode_Up;
	timer_init.TIM_Period = SG90_PWM_PERIOD_US - 1U;
	timer_init.TIM_ClockDivision = TIM_CKD_DIV1;
	timer_init.TIM_RepetitionCounter = 0U;
	TIM_TimeBaseInit(TIM4, &timer_init);

	/* 配置通道 1 为高电平有效的 PWM1 模式。 */
	TIM_OCStructInit(&pwm_init);
	pwm_init.TIM_OCMode = TIM_OCMode_PWM1;
	pwm_init.TIM_OutputState = TIM_OutputState_Enable;
	pwm_init.TIM_OCPolarity = TIM_OCPolarity_High;
	pwm_init.TIM_Pulse = SG90_PULSE_CENTER_US;
	TIM_OC1Init(TIM4, &pwm_init);
	TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable);
	TIM_ARRPreloadConfig(TIM4, ENABLE);

	/* 启动定时器,开始从 PB6 输出 PWM。 */
	TIM_Cmd(TIM4, ENABLE);
}

/*
 * 设置舵机角度。
 * 采用整数运算将 0°~180°线性换算为 500~2500 us,
 * 加入一半除数用于四舍五入,避免使用浮点运算占用额外资源。
 */
void SG90_SetAngle(uint16_t angle)
{
	uint32_t pulse_range;
	uint16_t pulse_us;

	/* 防止角度超过舵机允许的范围。 */
	if(angle > SG90_ANGLE_MAX)
	{
		angle = SG90_ANGLE_MAX;
	}

	pulse_range = SG90_PULSE_MAX_US - SG90_PULSE_MIN_US;
	pulse_us = (uint16_t)(SG90_PULSE_MIN_US +
		(((uint32_t)angle * pulse_range + (SG90_ANGLE_MAX / 2U)) /
		 SG90_ANGLE_MAX));

	sg90_angle = angle;
	sg90_pulse_us = pulse_us;
	/* CCR1 的数值就是高电平持续时间,单位为 us。 */
	TIM_SetCompare1(TIM4, pulse_us);
}

/*
 * 直接设置 PWM 高电平脉宽。
 * 该接口可用于不同舵机的行程标定,同时会反算并保存对应角度。
 */
void SG90_SetPulseWidth(uint16_t pulse_us)
{
	uint32_t pulse_offset;

	/* 将脉宽限制在 SG90 的安全控制范围内。 */
	if(pulse_us < SG90_PULSE_MIN_US)
	{
		pulse_us = SG90_PULSE_MIN_US;
	}
	else if(pulse_us > SG90_PULSE_MAX_US)
	{
		pulse_us = SG90_PULSE_MAX_US;
	}

	pulse_offset = pulse_us - SG90_PULSE_MIN_US;
	sg90_angle = (uint16_t)((pulse_offset * SG90_ANGLE_MAX +
		((SG90_PULSE_MAX_US - SG90_PULSE_MIN_US) / 2U)) /
		(SG90_PULSE_MAX_US - SG90_PULSE_MIN_US));
	sg90_pulse_us = pulse_us;
	TIM_SetCompare1(TIM4, pulse_us);
}

/* 将比较值清零,使 PB6 不再产生有效控制脉冲。 */
void SG90_Stop(void)
{
	TIM_SetCompare1(TIM4, 0U);
}

/* 返回最近一次设置或换算得到的角度。 */
uint16_t SG90_GetAngle(void)
{
	return sg90_angle;
}

/* 返回最近一次设置或换算得到的脉宽,单位为 us。 */
uint16_t SG90_GetPulseWidth(void)
{
	return sg90_pulse_us;
}

SG90.h代码

#ifndef __SG90_H
#define __SG90_H

#include "stm32f10x.h"

/*
 * SG90 舵机驱动参数
 * 控制周期:20 ms(频率为 50 Hz)
 * 高电平脉宽:500 us~2500 us,对应角度 0°~180°
 */
#define SG90_ANGLE_MIN          0U
#define SG90_ANGLE_MAX          180U
#define SG90_PULSE_MIN_US       500U
#define SG90_PULSE_CENTER_US    1500U
#define SG90_PULSE_MAX_US       2500U
#define SG90_PWM_PERIOD_US      20000U

/* 初始化 PB6 和 TIM4_CH1,舵机初始位置为 90° */
void SG90_Init(void);

/* 设置舵机角度,输入超过 180°时自动限制为 180° */
void SG90_SetAngle(uint16_t angle);

/* 直接设置高电平脉宽,输入值自动限制在 500~2500 us */
void SG90_SetPulseWidth(uint16_t pulse_us);

/* 停止输出 PWM;再次设置角度或脉宽即可恢复输出 */
void SG90_Stop(void);

/* 获取最近一次设置的舵机角度 */
uint16_t SG90_GetAngle(void);

/* 获取最近一次设置的高电平脉宽,单位为 us */
uint16_t SG90_GetPulseWidth(void);

#endif

6.2 源码编写特点

  1. 全部采用整数运算换算角度,无浮点运算,减少单片机算力消耗;
  2. 自带角度限幅保护,输入超过 180 或小于 0 自动钳位,防止堵转;
  3. 函数模块化封装,可单独调用任意角度,移植到其他项目只需要修改定时器引脚;
  4. 软件 I2C 驱动 OLED,无需硬件 I2C 引脚占用,兼容性更强。

七、实物演示效果章节

7.1 上电初始状态

开发板通电后,系统自动初始化定时器、OLED 屏幕、舵机外设,舵机直接转动至中位 90°。 OLED 屏幕固定显示头部信息:

SG90 SERVO DEMO
PB6 / TIM4_CH1
PWM:50Hz T:20ms
Angle:090 deg
Pulse:1500 us

7.2 实物现象观察要点

  1. 0° 时屏幕 Pulse 显示 500,180° 显示 2500,每递增 45° 脉宽增加 500us;
  2. 中位 90° 为 1500us,是舵机平衡位置;
  3. 若人为用手掰动舵盘,舵机会自动发力拉回程序设定角度,直观体现闭环控制特性;
  4. 调用 SG90_Stop 函数后,屏幕参数保留,但舵机失去锁止力,轻轻一推即可转动。

八、常见故障现象与排查方案

故障 1:舵机完全不转动

  1. 核查橙色信号线是否接 PB6;
  2. 确认舵机 5V 供电稳定,空载电压达标;
  3. 重点检查单片机与舵机电源共地线是否连通;
  4. 使用逻辑分析仪 / 示波器测量 PB6 引脚,确认存在 20ms 周期脉冲。

故障 2:舵机持续高频抖动

  1. 舵机供电功率不足,更换独立 5V 电源;
  2. 地线虚接、接触不良,参考电平波动;
  3. 机械结构卡滞、负载过重;
  4. 设定脉宽接近 0°/180° 机械极限,舵机持续堵转;
  5. 信号线过长,无屏蔽导致电磁干扰。

故障 3:屏幕角度和舵机实际转角不一致

OLED 仅显示程序下发的理论目标值,SG90 存在齿轮间隙、个体公差,属于正常现象。 解决办法:调用 SG90_SetPulseWidth 手动微调最小、最大脉宽,重新标定舵机实际限位。

故障 4:舵机转到两端限位发出持续嗡嗡声

代表脉宽超出舵机机械行程,电机堵转。立刻修改参数缩小脉宽区间,长时间堵转会烧毁舵机内部线圈、打坏齿轮。

故障 5:执行 SG90_Stop 后舵机不受控

该函数仅关闭 PWM 脉冲输出,不会切断舵机 5V 供电。失去脉冲信号后舵机无锁止力矩,负载会带动转轴偏移,属于正常设计。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐