1. 项目概述
最近在研究伺服驱动器方案时,发现基于STM32F103的方案在中小功率应用中性价比极高。这个量产级的伺服驱动器方案包含了完整的硬件设计和软件实现,特别适合想要深入理解伺服控制原理的工程师。方案采用了三块PCB板的设计:主控板负责算法处理,驱动板实现功率放大,显示板提供人机交互,形成了一个完整的伺服控制系统。
伺服驱动器的核心在于精确的位置控制和快速的动态响应。这个方案实现了两种运行模式:增量式编码器找零模式和开环运行模式,同时还支持省线式编码器和UVW增量式编码器的自动位置识别。在工业自动化领域,这种设计思路非常实用,既能满足高精度定位需求,又能适应不同的编码器类型。
提示:伺服驱动器的开发难点在于硬件电路的抗干扰设计和软件算法的实时性保证,这个方案在这两方面都做了很好的平衡。
2. 硬件设计解析
2.1 主控板电路设计
主控板以STM32F103C8T6为核心,这是一款72MHz主频的Cortex-M3内核MCU,具有丰富的外设资源。电路设计中特别注意了以下几点:
-
电源部分采用两级滤波设计,先经过LC滤波再通过LDO稳压,确保MCU供电稳定。实测中,这种设计能将电源纹波控制在50mV以内。
-
时钟电路使用8MHz晶振配合22pF负载电容,布局时使晶振尽量靠近MCU引脚,走线长度控制在10mm以内,避免时钟信号受到干扰。
-
所有GPIO口都设计了TVS二极管保护,特别是连接编码器的接口,采用了B5819WS二极管进行双向保护。
-
调试接口除了标准的SWD之外,还预留了USART1的TX/RX引脚,方便通过串口打印调试信息。
2.2 驱动板功率电路
驱动板采用三相全桥拓扑结构,关键元件选型如下:
| 元件类型 | 型号 | 参数 | 选型理由 |
|---|---|---|---|
| MOSFET | IPP60R099C6 | 600V/16A | 低导通电阻(99mΩ),开关速度快 |
| 驱动IC | IR2101S | 600V半桥驱动 | 自带死区控制,简化设计 |
| 电流传感器 | ACS712 | 5A量程 | 线性度好,成本低 |
| 电源芯片 | LM2596 | 3A降压 | 效率高,发热小 |
功率电路布局时特别注意了以下几点:
- 大电流走线宽度不小于2mm,表层铺锡增加载流能力
- 高低压区域用2mm的隔离槽分隔
- MOSFET散热片通过导热垫与外壳接触,实测满载温升不超过40°C
2.3 显示板人机交互
显示板采用128x64的OLED屏幕,通过I2C与主控通信。板上设计了5个按键和3个LED指示灯:
- 按键采用矩阵扫描方式,通过74HC165扩展IO
- LED驱动使用恒流源电路,确保亮度一致
- 所有信号线都加了RC滤波,防止误触发
3. 软件架构与实现
3.1 系统初始化流程
系统上电后按以下顺序初始化:
- 时钟系统初始化:先配置PLL将系统时钟升至72MHz
- GPIO初始化:按功能分组配置,特别注意复用功能引脚的配置
- 定时器初始化:
- TIM1用于PWM生成,中心对齐模式,死区时间设置为1us
- TIM2/TIM4用于编码器接口模式
- TIM3用于系统时基,产生1ms中断
- ADC初始化:配置规则组和注入组,用于电流电压采样
- 通信接口初始化:USART1用于调试,USART2用于与上位机通信
c复制void System_Init(void)
{
RCC_Configuration(); // 时钟配置
GPIO_Configuration(); // GPIO初始化
NVIC_Configuration(); // 中断配置
TIM_Configuration(); // 定时器配置
ADC_Configuration(); // ADC配置
USART_Configuration();// 串口配置
Encoder_Init(); // 编码器初始化
PID_Init(); // PID参数初始化
}
3.2 编码器处理算法
3.2.1 增量式编码器解码
使用TIM2和TIM4的编码器接口模式,配置为双边沿计数:
c复制void Encoder_Init(void)
{
TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12,
TIM_ICPolarity_Rising,
TIM_ICPolarity_Rising);
TIM_SetCounter(TIM2, 0);
TIM_Cmd(TIM2, ENABLE);
}
位置计算时需要考虑计数溢出情况:
c复制int32_t Get_Encoder_Position(void)
{
static uint16_t last_count = 0;
static int32_t total_count = 0;
uint16_t current_count = TIM_GetCounter(TIM2);
int16_t diff = (int16_t)(current_count - last_count);
if(diff > ENCODER_MAX/2) {
diff -= ENCODER_MAX;
} else if(diff < -ENCODER_MAX/2) {
diff += ENCODER_MAX;
}
total_count += diff;
last_count = current_count;
return total_count;
}
3.2.2 UVW编码器位置检测
UVW信号通过GPIO读取,每60°电角度为一个区间:
c复制uint8_t Get_UVW_Position(void)
{
uint8_t u = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0);
uint8_t v = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1);
uint8_t w = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2);
if(u && !v && !w) return 0;
if(u && v && !w) return 1;
if(!u && v && !w) return 2;
if(!u && v && w) return 3;
if(!u && !v && w) return 4;
if(u && !v && w) return 5;
return 0xFF; // 无效状态
}
3.3 PID控制算法实现
位置环和速度环采用串级PID结构:
c复制typedef struct {
float Kp;
float Ki;
float Kd;
float integral;
float prev_error;
float output;
float max_output;
} PID_TypeDef;
void PID_Calculate(PID_TypeDef* pid, float target, float feedback)
{
float error = target - feedback;
// 比例项
pid->output = pid->Kp * error;
// 积分项
pid->integral += pid->Ki * error;
if(pid->integral > pid->max_output) pid->integral = pid->max_output;
if(pid->integral < -pid->max_output) pid->integral = -pid->max_output;
pid->output += pid->integral;
// 微分项
pid->output += pid->Kd * (error - pid->prev_error);
pid->prev_error = error;
// 输出限幅
if(pid->output > pid->max_output) pid->output = pid->max_output;
if(pid->output < -pid->max_output) pid->output = -pid->max_output;
}
4. 关键功能实现细节
4.1 自动找零功能实现
省线式编码器找零流程:
- 电机以低速向一个方向旋转
- 检测Z信号上升沿
- 检测到Z信号后立即停止电机
- 将当前位置设为机械零点
- 将零点位置存入EEPROM
c复制void Auto_Zero_Search(void)
{
Set_Motor_Speed(LOW_SPEED); // 设置低速
while(1) {
if(READ_Z_SIGNAL()) { // 检测Z信号
Set_Motor_Speed(0); // 停止电机
current_position = 0; // 重置位置
EEPROM_Write(ZERO_POS_ADDR, 0);
break;
}
}
}
4.2 相序交换功能
相序交换通过重新映射PWM输出实现:
c复制void Swap_Phase_Sequence(void)
{
// 重新配置TIM1通道输出
GPIO_PinRemapConfig(GPIO_Remap_TIM1, ENABLE);
// 更新PWM输出顺序
TIM_OC1Init(TIM1, &TIM_OCInitStructure); // 原CH1->CH3
TIM_OC3Init(TIM1, &TIM_OCInitStructure); // 原CH3->CH1
}
4.3 串口通信协议
自定义了简单的通信协议:
| 字节 | 含义 | 说明 |
|---|---|---|
| 0x55 | 帧头 | 固定值 |
| CMD | 命令字 | 见命令表 |
| LEN | 数据长度 | 0-255 |
| DATA | 数据 | 可变长度 |
| SUM | 校验和 | 前面所有字节累加和 |
常用命令示例:
- 0x01:设置目标位置
- 0x02:读取当前位置
- 0x03:设置PID参数
- 0x04:保存参数到EEPROM
5. 调试经验与问题排查
5.1 常见问题及解决方案
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 电机抖动 | PID参数不合适 | 先调P,再调I,最后调D |
| 位置不准 | 编码器信号干扰 | 检查编码器线屏蔽层接地 |
| 过流保护 | MOSFET驱动不足 | 检查驱动电阻是否合适 |
| 通信失败 | 波特率不匹配 | 检查两端波特率设置 |
| 发热严重 | 死区时间不足 | 增加死区时间设置 |
5.2 电流采样注意事项
- 采样电阻要选用高精度、低温漂的型号,如1%精度的合金电阻
- 采样时机要在PWM周期的中间位置,避开开关瞬态
- 软件上需要做滑动平均滤波,窗口大小建议8-16点
c复制#define SAMPLE_NUM 16
uint16_t current_samples[SAMPLE_NUM];
uint8_t sample_index = 0;
uint16_t Get_Filtered_Current(void)
{
uint32_t sum = 0;
for(uint8_t i=0; i<SAMPLE_NUM; i++) {
sum += current_samples[i];
}
return sum / SAMPLE_NUM;
}
void ADC_IRQHandler(void)
{
if(ADC_GetITStatus(ADC1, ADC_IT_JEOC)) {
current_samples[sample_index++] = ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_1);
if(sample_index >= SAMPLE_NUM) sample_index = 0;
ADC_ClearITPendingBit(ADC1, ADC_IT_JEOC);
}
}
5.3 抗干扰设计要点
- 电源入口处加共模电感,推荐型号DLW21HN系列
- 所有IC的VCC引脚就近放置0.1uF陶瓷电容
- 编码器信号线采用双绞线,并加磁环
- 软件上对关键信号进行多次采样取中值
- 在PWM周期开始时关闭ADC采样,避免开关噪声影响
6. 性能优化技巧
6.1 实时性优化
- 将PID计算放在定时器中断中,确保控制周期稳定
- 使用DMA传输ADC采样结果,减少CPU开销
- 关键代码用汇编优化,特别是乘除法运算
assembly复制; 快速整数乘法优化
__asm int32_t __SMULBB(int32_t op1, int32_t op2)
{
smulbb r0, r0, r1
bx lr
}
6.2 内存优化
- 将const数据放在Flash中,使用__attribute__((section(".rodata")))
- 频繁访问的变量定义为register类型
- 使用位域结构体节省内存空间
c复制typedef struct {
uint8_t motor_enabled : 1;
uint8_t homing_done : 1;
uint8_t fault_status : 3;
uint8_t reserved : 3;
} System_Status_TypeDef;
6.3 功耗优化
- 在空闲时进入STOP模式,通过外部中断唤醒
- 动态调整PWM频率,低速时降低频率
- 关闭未使用的外设时钟
c复制void Enter_Low_Power_Mode(void)
{
// 关闭外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_USART1, DISABLE);
// 配置唤醒源
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line0; // PA0作为唤醒源
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
// 进入STOP模式
PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI);
// 唤醒后重新初始化系统
System_Init();
}
在实际项目中,我发现伺服驱动器的性能很大程度上取决于细节处理。比如PWM死区时间的设置,理论上计算可能需要100ns,但实际测试发现设置为1us时系统最稳定。又比如PID参数整定,不能只看理论计算,必须结合实际负载特性反复调试。
