简介:本资源是一个面向嵌入式开发初学者与STM32进阶实践者的三轴云台控制系统Keil工程源码包,聚焦于俯仰(Pitch)、翻滚(Roll)、偏航(Yaw)三自由度姿态稳定与电机协同控制,适用于无人机云台、智能摄影稳定器等硬件项目开发。压缩包共211个文件,主体为89个C源文件(含stm32f10x_tim.c、stm32f10x_rcc.c等标准外设库驱动)与95个H头文件(定义传感器接口、PID参数、硬件抽象层),辅以8个汇编启动文件、1个Keil工程配置文件(.uvproj)及.map、.lst等调试支持文件,整体仅324KB,结构紧凑、模块清晰,便于理解底层驱动与控制逻辑的耦合关系。目前已有64人学习下载。读者可直接导入Keil μVision 5环境编译运行,完整掌握基于STM32F1系列的IMU数据采集、PID闭环调节、PWM电机驱动及多任务时序调度等核心实现,是深入理解嵌入式云台控制系统的优质实操范例。
1. 这不是普通 Keil 工程:它是一套可烧录、可调试、带完整 TIM+RCC 驱动的 STM32F10x 三轴云台底层控制骨架
你拿到的三轴云台Keil工程.7z,表面看是几个.bak文件和重复的.c源码,但实际是嵌入式工程师在真实硬件上跑通三轴电机闭环前最关键的「最小可行驱动层」——它不包含 PID 参数整定逻辑,也不含 IMU 数据融合代码,却已把 STM32F10x 的定时器高级控制通道(TIM1/TIM8)、PWM 输出极性配置、主从同步触发、RCC 时钟树分频关系全部固化进stm32f10x_tim.c和stm32f10x_rcc.c中。这意味着:只要接上三路 BLDC 或步进驱动板,改几行#define MOTORx_TIM_CHANNEL,就能输出三组相位可控、死区可调、频率锁定的 PWM 波;而stm32f10x_flash.c则预留了参数存储区(非易失),用于保存云台零点偏移、PID 增益等运行时可调量。它适合两类人:一是刚学完《STM32F10x 参考手册》第 14/15/16 章的中级开发者,需要跳过寄存器配置陷阱直接验证电机响应;二是做毕业设计或产品原型的工程师,需快速复用经过实测的时钟初始化与 PWM 同步逻辑,避免因RCC_CFGR |= (uint32_t)RCC_CFGR_PPRE2_2;配错导致 TIM1 输入捕获失锁这类隐蔽问题。
2. 为什么必须从 RCC 时钟树和 TIM 高级定时器切入:三轴云台对 PWM 同步性的硬约束
2.1 三轴云台的物理同步需求倒逼时钟与定时器选型
三轴云台的俯仰(Pitch)、翻滚(Roll)、偏航(Yaw)三路电机必须在微秒级时间窗口内完成相位对齐,否则会出现耦合振荡。例如当 Pitch 轴执行快速抬升动作时,若 Roll 轴 PWM 更新滞后 2μs,机械臂会产生瞬时扭矩差,引发高频抖动。这种现象在空载测试中不易察觉,但加载摄像头模组后会直接表现为画面“果冻效应”。因此,工程中未使用通用定时器(TIM2–TIM4),而是强制绑定到高级定时器 TIM1(主)与 TIM8(从),利用其BDTR寄存器中的MOE(Main Output Enable)位实现硬件级使能同步,并通过TIM_SelectOutputTrigger(TIM1, TIM_TRGO_Update)将 TIM1 的更新事件作为 TIM8 的触发源。这种设计绕过了软件延时或中断嵌套带来的不确定性,是工业级云台控制的起点。
提示:查看
stm32f10x_rcc.c中RCC_Configuration()函数,重点注意RCC_HCLKConfig(RCC_SYSCLK_Div1);与RCC_PCLK2Config(RCC_HCLK_Div2);的组合——PCLK2 必须为 HCLK 的整数分频,否则 TIM1 的计数周期将漂移。STM32F10x 数据手册明确要求:当 APB2 总线(挂载 TIM1)分频系数为 2 时,TIM1 时钟 = PCLK2 × 2,此倍频关系是 PWM 分辨率计算的根基。
2.2 TIM 初始化代码深度解析:从寄存器映射到电机驱动语义
以下是从stm32f10x_tim.c中提取并注释的核心初始化片段,对应三轴 PWM 输出通道配置:
void TIM1_PWM_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; GPIO_InitTypeDef GPIO_InitStructure; // 1. 使能 TIM1 和相关 GPIO 时钟(APB2) RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); // 2. 配置 PA8/PA9/PB13 为复用推挽(TIM1_CH1/CH2/CH3) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; GPIO_Init(GPIOB, &GPIO_InitStructure); // 3. 定时器基础参数:16MHz 输入 → 1MHz 计数频率 → 1μs 分辨率 // 注意:此处 16MHz 来自 RCC_CFGR_PPRE2=2 → PCLK2=36MHz → TIM1_CLK=72MHz → 实际分频后为 1MHz TIM_TimeBaseStructure.TIM_Period = 999; // 自动重装载值:1000 个计数周期 = 1ms 周期 TIM_TimeBaseStructure.TIM_Prescaler = 71; // 预分频器:72MHz / (71+1) = 1MHz TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); // 4. 通道1(Pitch):中央对齐模式 + 死区插入(防桥臂直通) TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable; // 启用互补通道 TIM_OCInitStructure.TIM_Pulse = 500; // 初始占空比 50% TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High; TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Reset; TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCIdleState_Reset; TIM_OC1Init(TIM1, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); // 5. 关键:启用刹车与死区(BDTR) TIM_BDTRInitTypeDef TIM_BDTRInitStructure; TIM_BDTRInitStructure.TIM_OSSRState = TIM_OSSRState_Enable; // 运行模式下保持输出 TIM_BDTRInitStructure.TIM_OSSIState = TIM_OSSIState_Enable; // 空闲模式下强制低电平 TIM_BDTRInitStructure.TIM_LOCKLevel = TIM_LOCKLevel_1; // 锁定级别1(防误写) TIM_BDTRInitStructure.TIM_DeadTime = 0x1F; // 死区时间 = 31 × Tdts(约 310ns) TIM_BDTRInitStructure.TIM_Break = TIM_Break_Disable; // 禁用外部刹车 TIM_BDTRConfig(TIM1, &TIM_BDTRInitStructure); // 6. 全局使能主输出(MOE),否则 PWM 无输出 TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); }参数说明与实操要点:
TIM_Prescaler = 71对应 72MHz 主频下的 1MHz 计数频率,这是实现 1μs 级 PWM 调节精度的前提。若你的晶振为 8MHz,需同步修改RCC_PLLMul_x并重算预分频值;TIM_DeadTime = 0x1F是经验安全值,针对 IR2104 类驱动芯片,若换用 DRV8301 需降至0x0A,否则死区过大会降低有效电压;TIM_OSSIState_Enable在 MCU 复位期间强制输出低电平,防止电机意外启动,此配置在云台断电重启场景中至关重要;- 所有
TIM_OCxInit()调用后必须跟TIM_OCxPreloadConfig(),否则 CCRx 寄存器更新不同步,三轴 PWM 相位会随机偏移。
2.3 工程文件结构还原:.bak文件揭示的开发迭代痕迹
压缩包中出现多个.bak文件并非冗余备份,而是 Keil 工程配置演进的关键线索:
| 文件名 | 含义 | 实操价值 |
|---|---|---|
STM32-DEMO.uvgui_Administrator.bak | 上一版 GUI 配置(含调试器设置、内存映射) | 若当前 uVision 报错 "Target not connected",可对比此文件中Debug\Settings\JTAG的 Speed 设置是否与 ST-Link 固件版本匹配 |
STM32-DEMO_uvproj.bak | 上一版工程结构(含源文件路径、宏定义) | 当新增motor_control.c后编译报undefined reference to 'Motor_SetSpeed',需检查此 bak 文件中Groups\MotorDriver下是否遗漏添加.c文件 |
STM32-DEMO_uvopt.bak | 上一版编译选项(优化等级、C99 支持、浮点单元) | 若启用--fpu=vfp但未勾选Use MicroLIB,会导致printf占用超 8KB Flash,此时应恢复 bak 中Optimization Level = 2的设定 |
这些.bak文件本质是 Keil 的「配置快照」,建议在修改工程属性前手动复制当前.uvproj为新.bak,形成可回溯的配置链。
3. 从裸机驱动到闭环控制:在现有工程上叠加 PID 与角度反馈的四步落地法
3.1 硬件信号链确认:三轴 PWM 输出与编码器/霍尔输入的物理映射
在烧录前,必须核对原理图与代码的引脚一致性。本工程默认采用如下映射(以 STM32F103C8T6 为例):
| 云台轴向 | PWM 输出引脚 | 定时器通道 | 编码器 A/B 相输入 | GPIO 端口/引脚 | 备注 |
|---|---|---|---|---|---|
| Pitch | PA8 | TIM1_CH1 | PA0/PA1 | GPIOA Pin0/1 | 使用 TIM2 编码器接口模式 |
| Roll | PA9 | TIM1_CH2 | PB6/PB7 | GPIOB Pin6/7 | TIM4 编码器接口 |
| Yaw | PB13 | TIM1_CH3 | PB8/PB9 | GPIOB Pin8/9 | TIM3 编码器接口 |
注意:
stm32f10x_tim.c中仅初始化了 PWM 输出,未包含编码器接口配置。需自行添加TIM_EncoderInterfaceConfig()调用,并确保RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2 | RCC_APB1Periph_TIM3 | RCC_APB1Periph_TIM4, ENABLE);已启用对应定时器时钟。
3.2 PID 控制器嵌入:在main.c中插入增量式 PID 计算循环
在while(1)主循环中插入以下结构(无需 RTOS,纯裸机):
// 全局变量(定义在 main.c 顶部) volatile int16_t pitch_angle = 0, roll_angle = 0, yaw_angle = 0; // 来自编码器解码 int16_t pitch_target = 0, roll_target = 0, yaw_target = 0; // 目标角度(可由串口/遥控器更新) int16_t pitch_pwm = 0, roll_pwm = 0, yaw_pwm = 0; // PWM 输出值 // PID 参数(可存入 Flash,此处为初始值) #define KP 120 #define KI 2 #define KD 8 int32_t pitch_err_sum = 0, pitch_err_last = 0; int32_t roll_err_sum = 0, roll_err_last = 0; int32_t yaw_err_sum = 0, yaw_err_last = 0; // 主循环内调用 void PID_Calculate(void) { int32_t err_pitch = pitch_target - pitch_angle; int32_t err_roll = roll_target - roll_angle; int32_t err_yaw = yaw_target - yaw_angle; // 增量式 PID(抗积分饱和) pitch_pwm += KP * (err_pitch - pitch_err_last) + KI * err_pitch + KD * (err_pitch - 2*pitch_err_last + pitch_err_last_last); pitch_err_last_last = pitch_err_last; pitch_err_last = err_pitch; if (pitch_pwm > 999) pitch_pwm = 999; if (pitch_pwm < -999) pitch_pwm = -999; // 同理处理 roll/yaw... // (此处省略,结构完全一致) // 写入 CCR 寄存器(注意符号:负值表示反向旋转) if (pitch_pwm >= 0) { TIM_SetCompare1(TIM1, pitch_pwm); GPIO_ResetBits(GPIOA, GPIO_Pin_10); // 方向引脚置低 } else { TIM_SetCompare1(TIM1, -pitch_pwm); GPIO_SetBits(GPIOA, GPIO_Pin_10); // 方向引脚置高 } }关键逻辑说明:
pitch_pwm为有符号整数,正负决定电机转向,绝对值决定 PWM 占空比,避免使用abs()函数减少运算开销;KI * err_pitch实现积分项,但未做限幅,实际应用中需加入if (pitch_err_sum > 5000) pitch_err_sum = 5000;防止积分饱和;TIM_SetCompare1()直接操作影子寄存器,确保更新时刻精确到下一个更新事件,与TIM1->CCR1 = value效果相同但更符合 Keil 标准外设库习惯。
3.3 编码器角度解算:基于 TIM2 的正交解码与 32 位计数扩展
STM32F10x 的通用定时器支持硬件正交解码,但计数器为 16 位,易溢出。需在溢出中断中扩展为 32 位:
// 在 stm32f10x_it.c 中添加 extern volatile uint32_t pitch_count; // 全局 32 位计数器 void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { if (TIM2->CR1 & TIM_CR1_DIR) { // 计数方向:1=递减 pitch_count--; } else { pitch_count++; } TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } } // 初始化函数(在 main() 中调用) void Encoder_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // PA0/PA1 配置为浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); // TIM2 编码器模式:TI1FP1 和 TI2FP2 TIM_TimeBaseStructure.TIM_Period = 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler = 0; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_ICStructInit(&TIM_ICInitStructure); TIM_ICInit(TIM2, &TIM_ICInitStructure); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); TIM_Cmd(TIM2, ENABLE); }参数说明:
TIM_EncoderMode_TI12表示同时使用通道 1 和 2 的上升沿触发,四倍频提升分辨率;TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE)启用溢出中断,每 65536 个脉冲触发一次,配合pitch_count实现 32 位计数;- 若编码器线数为 1000 线,则每转产生 4000 个计数脉冲,
pitch_count / 4000.0f即为当前圈数,乘以 360 得角度值。
4. Keil 工程复现与排错:从 uVision5 环境搭建到 AXF 生成失败的七类高频问题
4.1 Keil MDK-ARM 环境必备组件安装清单(基于 uVision5 v5.38+)
| 组件 | 安装方式 | 版本要求 | 验证方法 |
|---|---|---|---|
| ARM Compiler 5 | Keil 安装包内置 | v5.06 update 6+ | Project → Options → Target → ARM Compiler 版本号 |
| STM32F1xx Device Family Pack | Pack Installer → Search "STM32F1" → Install | v2.3.0+ | Project → Manage → Pack Installer → 已安装列表 |
| ST-Link Debugger Driver | ST 官网下载stsw-link009 | v6.3.0+ | 设备管理器中显示 "STMicroelectronics ST-LINK/V2-1" |
| µVision Debugger Configuration | Project → Options → Debug → Settings → Port: ST-Link | 必须勾选 "Reset and Run" | 点击 Download 后自动复位运行 |
提示:若 uVision 报错 "Device not found",90% 情况是 Pack 未安装或版本过低。打开
C:\Keil_v5\ARM\PACK\Keil\STM32F1xx_DFP\2.3.0\目录,确认存在SVD\STM32F103xx.svd文件,此文件为调试器识别芯片型号的依据。
4.2 AXF 生成失败的七类典型错误及修复命令
当点击 Build 后出现".axf: Error: L6218E: Undefined symbol"或".axf: Error: L6915E: Library reports error"时,按以下顺序排查:
| 错误现象 | 根本原因 | 修复命令/操作 | 说明 |
|---|---|---|---|
Undefined symbol 'SystemInit' | 启动文件缺失或未添加到工程 | 右键 Project → Add Group → 命名为 "Startup" → 右键该组 → Add Files → 选择startup_stm32f10x_md.s | 此文件位于Keil_v5\ARM\PACK\Keil\STM32F1xx_DFP\2.3.0\Device\Source\ARM\ |
L6218E: Undefined symbol 'USART1_IRQHandler' | 中断服务函数未定义 | 在stm32f10x_it.c中添加void USART1_IRQHandler(void) { /* 清除中断标志 */ } | Keil 要求所有声明的中断向量必须有对应函数体,即使为空 |
Error: #5: cannot open source input file "stm32f10x.h" | 头文件路径未配置 | Project → Options → C/C++ → Include Paths → 添加C:\Keil_v5\ARM\PACK\Keil\STM32F1xx_DFP\2.3.0\Device\Include\ | 路径末尾不能有\,否则 Keil 解析失败 |
Error: #20: identifier "GPIO_Pin_10" is undefined | 宏定义未启用 | 在stm32f10x.h顶部取消注释#define STM32F10X_MD | 此宏决定启用中密度芯片的寄存器定义 |
Error: L6218E: Undefined symbol 'HAL_Delay' | 混用 HAL 库与标准外设库 | 删除所有#include "stm32f1xx_hal.h"及HAL_*函数调用 | 本工程基于标准外设库(StdPeriph),与 HAL 不兼容 |
Error: L6915E: Library reports error | ARM Compiler 版本与 Pack 不匹配 | Project → Options → Target → ARM Compiler → Version 选择 "Use default compiler version" | 强制使用 Pack 指定的编译器版本 |
Warning: #1-D: last line of file ends without a newline | 某个.c文件末尾缺换行符 | 用 Notepad++ 打开报错文件 → 编辑 → EOL Conversion → Unix (LF) | Keil 对文件格式敏感,Windows (CRLF) 可能导致预处理器异常 |
4.3 烧录后电机无响应的三级定位法
当程序编译通过且成功下载,但三路电机均无动作时,按以下顺序快速定位:
- 电源与使能信号层:用万用表测量驱动板
EN引脚电压,确认是否为 3.3V(高电平使能)。若为 0V,检查GPIO_ResetBits(GPIOx, GPIO_Pin_y)是否误将使能引脚拉低; - PWM 波形层:用示波器探头接 PA8,观察是否有 1kHz 方波(
TIM_Period=999对应 1ms 周期)。若无波形,检查TIM_Cmd(TIM1, ENABLE)是否被注释,或TIM_CtrlPWMOutputs(TIM1, ENABLE)是否缺失; - 死区与刹车层:测量 PA8 与 PA9(互补通道)波形相位差,正常应为 180° 反相且存在死区空白。若两路同相或无死区,检查
TIM_BDTRConfig()中TIM_DeadTime是否为 0,或TIM_BDTRInitStructure.TIM_LOCKLevel是否过高导致寄存器写保护。
5. 实战技巧:用 Keil Logic Analyzer 实时观测三轴 PWM 占空比与角度误差曲线
5.1 配置 SWO 输出通道映射到 ITM Stimulus Ports
Keil uVision5 内置的 Logic Analyzer 可直接捕获变量变化,无需额外硬件。关键在于将目标变量路由至 SWO(Serial Wire Output):
// 在 main.c 开头添加 #include "core_cm3.h" #include "itm.h" // 在 main() 开始处初始化 ITM void ITM_Init(void) { if ((CoreDebug->DEMCR & CoreDebug_DEMCR_TRCENA_Msk) == 0) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; } if ((ITM->TCR & ITM_TCR_ITMENA_Msk) == 0) { ITM->TCR |= ITM_TCR_ITMENA_Msk; } if ((ITM->TER & 0x01) == 0) { ITM->TER |= 0x01; } } // 在 while(1) 中添加(每 10ms 发送一次) if (SysTick_GetFlag()) { ITM_SendChar(0x00); // Port 0: pitch_pwm ITM_SendShort(pitch_pwm); ITM_SendChar(0x01); // Port 1: pitch_angle ITM_SendShort(pitch_angle); ITM_SendChar(0x02); // Port 2: pitch_target ITM_SendShort(pitch_target); }配置步骤:
- Project → Options → Debug → Settings → Trace → Core Clock 设置为实际系统时钟(如 72MHz);
- View → Serial Wire Viewer → ITM Data Console → 点击右上角齿轮图标 → Enable Port 0/1/2;
- View → Analysis Windows → Logic Analyzer → Add Signal → 输入
pitch_pwm@Port0、pitch_angle@Port1; - 点击 Run,即可实时绘制三轴角度误差(
target - angle)与 PWM 输出的关联曲线。
5.2 利用 .map 文件精确定位 Flash 占用瓶颈
编译后生成的STM32-DEMO.map文件是优化代码体积的关键依据。重点关注以下三段:
******************************************************************************* *** SECTION SUMMARY ******************************************************************************* Name Size Addr .text 12452 0x08000000 ← 代码段总大小(本工程约 12KB) .data 40 0x20000000 ← RAM 初始化数据(全局变量初值) .bss 128 0x20000028 ← RAM 未初始化区(静态变量) ******************************************************************************* *** SYMBOL TABLE ******************************************************************************* 0x080003a0 0x1e4 stm32f10x_tim.o(.text) ← TIM 驱动代码占 484 字节 0x08000584 0x15c stm32f10x_rcc.o(.text) ← RCC 配置占 348 字节 0x080006e0 0x2a0 main.o(.text) ← main.c 占 672 字节(含 PID 计算)实战技巧:
- 若
.text接近 64KB(STM32F103C8T6 Flash 上限),优先裁剪printf相关函数:Project → Options → C/C++ →--no_unaligned_access --no_fpu并禁用Use MicroLIB; main.o(.text)过大说明 PID 计算过于复杂,可将KD项改为查表法(int8_t kd_table[10] = {...}),减少乘法运算;.bss区域过大(>512 字节)表明静态数组过多,应将大缓冲区(如int16_t encoder_buffer[1024])移至malloc()动态分配,避免占用固定 RAM。
提示:在
STM32-DEMO.map文件末尾的Memory Map of the image部分,查找ER_IROM1段的Max Length与Actual Usage,二者差值即为剩余 Flash 空间。当Actual Usage>Max Length * 0.9时,必须启动代码瘦身流程。
本文还有配套的精品资源,点击获取