如何实现STM32F103直流有刷电机速度闭环控制算法优化?

摘要:在《基于直流有刷电机的速度闭环控制以及matlab仿真》我们介绍了速度闭环控制的实现,其采用的是PID控制算法,本节我们就基于STM32F103来实现直流电机的增量式PID速度闭环控制。 一、软件实现 我们需要将PID控制器应用于STM32
在《基于直流有刷电机的速度闭环控制以及matlab仿真》我们介绍了速度闭环控制的实现,其采用的是PID控制算法,本节我们就基于STM32F103来实现直流电机的增量式PID速度闭环控制。 一、软件实现 我们需要将PID控制器应用于STM32F103的速度闭环电机控制,以下是接下来的步骤: 电机速度测量:使用编码器来测量电机的实际转速,需要配置定时器的编码器模式来读取编码器脉冲,从而计算速度;具体参考《STM32F103霍尔编码器测速》; 电机驱动:通常使用PWM驱动电机,需要配置定时器的PWM输出模式,并设置占空比来控制电机速度;具体参考《基于直流有刷电机的开环调速控制以及matlab仿真》; PID周期调控:PID计算需要定期执行,因此需要配置一个定时器中断,在中断服务函数中执行PID计算和更新PWM输出; 主程序初始化:初始化所有外设(比如电机初始化、编码器初始化、中断定时器等)和PID参数; 主循环:通常主循环可以处理一些非实时任务,比如接收用户指令(例如通过串口改变目标速度)等。 1.1 PID算法实现 1.1.1 自定义PID结构体 在control目录下新建pid.h文件,自定义PID结构体; /******************************************************************************************************/ #ifndef _STM32f10x_PID__H #define _STM32f10x_PID__H #include "encoder.h" #include <stdio.h> #define MAX_HISTORY 300 // 最多保存300次数据 typedef struct { float Kp, Ki, Kd; // 比例、积分、微分系数 float Ts; // 调控周期 (秒) float prev_error; // e(k-1) float prev_error2; // e(k-2) float out_max; // 输出的最大值(占空比) float out_min; // 输出的最小值(占空比) float output; // 当前总输出 u(k)(占空比) float target_val; // 目标转速值 (RPM) // 历史记录 float measure_history[MAX_HISTORY]; // 测量转速记录 float output_history[MAX_HISTORY]; // 输出占空比记录 int record_count; // 已记录次数 int is_full; // 是否已存满 int has_output; // 标记是否已输出过 float raw_measure; // 当前测量转速值 r(k) } PID; extern PID g_pid; // 全局PID参数 extern void pid_param_init(void); // 初始化PID参数 extern void set_pid_params(float kp, float ki, float kd, int ts, int target_val); // 设置PID相关参数 extern float pid_compute(void); // PID计算 extern void print_measure_record(void); // 输出测量转速记录 #endif 在PID结构体中除了PID算法用到的一些参数外,我们还存储了当前测量的转速值、PID输出的占空比记录值。 1.1.2 PID参数初始化 在control目录下新建pid.c文件。pid_param_init函数用于初始化g_pid参数,比如: 将目标值g_pid.target_val设置为1500.00; 将实际值、上一次偏差值和上上次偏差值等初始化为0。 此外,我们将PID算法调控周期设置为10ms,这个在后面我们会单独介绍。 /*****************************************************************************/ #include "pid.h" PID g_pid; // 全局pid参数 /***************************************************************************** * * Description: 初始化PID参数 * ********************************************************************************/ void pid_param_init() { int i = 0; g_pid.target_val = 1500.0f; g_pid.Ts = 0.01f; g_pid.output = 0.0f; g_pid.prev_error = 0.0f; g_pid.prev_error2 = 0.0f; g_pid.out_max = 100.0f; g_pid.out_min = 0.0f; g_pid.Kp = 0.01f; g_pid.Ki = 0.01f; g_pid.Kd = 0.01f; // 历史记录初始化 g_pid.record_count = 0; g_pid.is_full = 0; g_pid.has_output = 0; g_pid.raw_measure = 0.0f; // 清空历史记录 for(i = 0; i < MAX_HISTORY; i++) { g_pid.measure_history[i] = 0; g_pid.output_history[i] = 0; } } 接着我们提供了set_pid_params函数修改PID参数; /***************************************************************************** * * Description: 设置PID相关参数 * Parameter : kp:比例环节系数 * ki:积分环节系数 * kd: 微分环节系数 * ts: PID算法调控周期 单位ms * target_val: 目标值 * ********************************************************************************/ void set_pid_params(float kp, float ki, float kd, int ts, int target_val) { g_pid.Kp = kp; g_pid.Ki = ki; g_pid.Kd = kd; g_pid.target_val = target_val; g_pid.Ts = ts/1000.0f; } 1.1.3 PID算法实现 增量式PID算法: \[Δu(k)=K_p[e(k)-e(k-1)]+K_iT_se(k)+\frac{K_d}{T_s}[e(k)-2e(k-1)+e(k-2)] \] \[u(k)=u(k-1)+Δu(k) \] 增量式PID算法实现: /************************************************************************************************************** * * Description: PID控制算法 * Return : PID输出值 * **************************************************************************************************************/ float pid_compute() { // 1. 获取编码器速度 float raw_measure = get_encoder_speed(g_pid.Ts*1000); // 2. 计算误差 float error = g_pid.target_val - raw_measure; // 3. 增量式PID计算 float delta_u = g_pid.Kp * (error - g_pid.prev_error) + g_pid.Ki * g_pid.Ts * error + (g_pid.Kd / g_pid.Ts) * (error - 2.0f * g_pid.prev_error + g_pid.prev_error2); // 4. 更新总输出 g_pid.output = g_pid.output + delta_u; // 5. 输出限幅 if (g_pid.output > g_pid.out_max) g_pid.output = g_pid.out_max; if (g_pid.output < g_pid.out_min) g_pid.output = g_pid.out_min; // 6. 更新误差历史 g_pid.prev_error2 = g_pid.prev_error; g_pid.prev_error = error; // 7. 记录数据用于调试 if(!g_pid.is_full){ g_pid.measure_history[g_pid.record_count] = raw_measure; g_pid.output_history[g_pid.record_count] = g_pid.output; g_pid.record_count++; // 检查是否已满 if(g_pid.record_count == MAX_HISTORY) { g_pid.is_full = 1; } } g_pid.raw_measure = raw_measure; return g_pid.output; } 1.1.4 存储转速数据 我们在进行PID参数调试的时候,通常会开发相应的上位机软件和下位机程序,但是这样周期就会变得很长,那有没有简单的方式呢? 这里我们将下位机测量的电机转速数据存储起来,然后通过串口发送到我们的串口助手里,有了这些数据后,我们就可以通过工具去分析电机对于目标转速的跟踪情况。 /************************************************************************************************************** * * Description: 输出测量记录 * **************************************************************************************************************/ void print_measure_record() { int i = 0; // 检查是否已经输出过 if(g_pid.has_output) { return; } if(g_pid.is_full == 0) { printf("转速数据未保存完毕!\n"); return; } printf("\n========== 转速历史数据 ==========\n"); printf("总记录数: %d\n", g_pid.record_count); // 简单输出,每行一个转速值 for(i = 0; i < g_pid.record_count; i++) { printf("%.1f\n", g_pid.measure_history[i]); } printf("========== 输出完成 ==========\n"); // 标记为已输出 g_pid.has_output = 1; } 1.2 PID周期调控 1.2.1 调控周期 离散PID程序实现的第一步就是,确定一个调控周期T ,每隔时间T, 程序执行一次PID调控。 至于调控周期设置多大比较好呢,这也是有说法的,调控周期一般取决于被控对象变化的快慢;比如倒立摆平衡车、四轴飞行器等,这些对象变化的很快,你总不能说我一秒才调控一次吧,要是等一秒才调控一次,那倒立摆、平衡车早就倒了,四轴飞行器早就掉下来了,所以这些对象调控一定要快,一般20 ms 、10 ms 、5ms甚至1ms就要进行一次调控。 调控的越快一般来说对象就会越稳定,但是调控周期也不能无限制的快,因为会受硬件传感器等设备分辨率限制,比如姿态传感器每隔5 ms才能更新一下数据,那么你PID 1ms就调控一次也没意义。 而且,对于电机速度控制来说,调控周期不需要特别快,一般20到100 ms调控一次就可以,电机控速调控也不能过快,因为会受编码器分辨率限制,调控周期越快,编码器测速的分辨率就越低,过快的调控周期,编码器测速根本不准确,这时再快的调控也没有意义。 最后,对于某些很大的被控对象,比如给一个很大的锅炉加热,或者给一个游泳池加热,这些对象的变化非常缓慢,PID调控周期也得慢下来,调快了没意义,比如几百毫秒、几秒甚至几十s调控一次都没问题. 因此,调控周期T到底确定为多久,需要根据被控对象变化的速度来决定,没有一个固定的值,需要靠我们的实践经验来反复调节尝试。 1.2.2 实现方案 那在程序中,如何实现每隔时间T执行一次调控呢?下面给出了三种实现方案: 方案 实现逻辑 优点 缺点 delay延时 主循环中执行PID后延时t 实现最简单 周期不精确,阻塞主程序 定时器中断 中断中直接执行PID调控 周期精确,不阻塞主程序 如果在主程序和中断中都对同一硬件进行数据的读取,可能引发硬件资源访问冲突 定时器 + 标志位 中断置标志位,主循环检测标志位执行PID 无资源冲突 主程序阻塞时周期不精确 这里我们采用定时器中断方式,因此需要配置一个定时器中断,在中断服务函数中执行PID计算和更新PWM输出。 1.2.3 实现代码 开启定时中断,这里使用的定时器6,在定时器溢出中断中执行PID运算。修改time.c文件: #include "pid.h" #include "motor.h" /******************************************************************************* * Function Name : TIM6_IRQHandler * Description : This function handles TIM6 global interrupt request. * Input : None * Output : None * Return : None *******************************************************************************/ void TIM6_IRQHandler(void) { //**********************自定义用户任务****************************// int res_pwm = 0; /*PWM值(PID输出)*/ // 1. 进行PID运算,得到PWM输出值 res_pwm = pid_compute(); // 2. 更新PWM输出 motor_set_duty(res_pwm); //*****************************************************************// TIM6->SR &= ~(1 << 0); // 清中断标志 } 其中motor_set_duty定义在motor.c文件中; /************************************************************************************************** * * Function : 设置电机PWM占空比 * Parameter : duty:PWM占空比 * **************************************************************************************************/ void motor_set_duty(int duty) { g_motor_pwm = duty; if(g_motor_pwm < 0) { g_motor_pwm = 0; } if(g_motor_pwm >100) { g_motor_pwm = 100; } motor_update_speed(); } 1.3 主程序 在主程序中初始化所有外设(比如电机初始化、编码器初始化、中断定时器等)和PID参数; #include "common.h" #include "stdio.h" int main() { int duty; int ts = 100; // PID算法调控周期 STM32_Clock_Init(9); // 系统时钟初始化 // 串口初始化 STM32_NVIC_Init(2, USART1_IRQn, 0, 1); // 串口中断优先级初始化,其中包括中断使能 usart_init(USART_1, 115200); // 串口1初始化,波特率115200 映射到PA9 PA10 // 按键KEY初始化 gpio_init(PC5, GPI_UP, HIGH); // PC5接按键KEY0 gpio_init(PA15, GPI_UP, HIGH); // PA15接按键KEY1 Ex_NVIC_Congig(PC5, FALLING); // 按键KEY0按下触发 高电平->低电平 Ex_NVIC_Congig(PA15, FALLING); // 按键KEY1按下触发 高电平->低电平 STM32_NVIC_Init(2, EXTI9_5_IRQn, 2, 2); // EXTI线[9:5]中断优先级初始化,其中包括中断使能 STM32_NVIC_Init(2, EXTI15_10_IRQn, 2, 2); // EXTI线[15:10]中断优先级初始化,其中包括中断使能 motor_init(); // 电机初始化,使用的定时器2,PA0/PA1 encoder_init(); // 编码器初始化,使用的定时器4,PB6/PB7 pid_param_init(); // 初始化PID参数 set_pid_params(0.01f, 0.0f, 0.0f, ts, 1500.0f); STM32_NVIC_Init(2, TIM6_IRQn, 0, 0); // TIM6溢出中端使能 TIM_Init_MS(TIMER6, ts); // TIM6计数到ts ms发生中断,进行PID周期调控 while (1) { printf("Output: %.2f, Speed: %.2f RPM, Error: %.2f\n", g_pid.output, g_pid.raw_measure, g_pid.prev_error); print_measure_record(); delay_ms(1000); } } 二、 测试 2.1 硬件接线 硬件接线参考《基于直流有刷电机的开环调速控制以及matlab仿真》。 2.2 软件设定 2.2.1 采样周期和目标转速设定 我们测速使用的775-P16霍尔编码器是低精度编码器; 脉冲数:16 PPR(Pulses Per Revolution); 4倍频后:64计数/转; 如果我们将目标转速设定为1500 rpm,采样周期设定为10ms; \[计数 = 1500 / 60 × 64 × 0.01 = 16个脉冲 \] 但如果测量误差1个脉冲: \[测速 = (15 / 64) / 0.01 × 60 ≈ 1406 rpm \] \[测速 = (17 / 64) / 0.01 × 60 ≈ 1594 rpm \] 1个脉冲误差将会造成94 rpm误差!因此可以看出霍尔编码器775-P16在测量低速的时候存在脉冲少,测速不准的问题。那我们怎么解决这个问题呢? 更换高分辨率编码器,比如使用500线光电编码器; 增加采样周期; 这里我们选择第二种方式。 2.2.1.1 提高采样周期 将采样周期设为20ms(与PID调控周期一致),此时1500 rpm对应的脉冲数: \[(1500×64×0.02)/60=32个脉冲 \] 1个脉冲误差将会造成1500 / 32 ≈ 47 rpm误差! 将采样周期设为100ms(与PID调控周期一致),此时1500 rpm对应的脉冲数: \[(1500×64×0.1)/60=160个脉冲 \] 1个脉冲误差将会造成1500 / 160 ≈ 9 rpm误差! 2.2.1.2 提高目标转速 如果我们将目标设定为9000 rpm,对应的脉冲数: \[(9000×64×0.02)/60=192个脉冲 \] 1个脉冲误差将会造成9000 / 192 ≈ 47 rpm误差!可以看到提高并没有降低误差带来的影响。 2.2.2 烧录测试 目标转速设置为1500 rpm,调控周期设置为100 ms,编译程序并下载测试,可以通过串口查看当前输出的占空比以及对应的转速; 2.3 PID参数整定 为了进行PID参数整定,我们通过串口输出了前300次的转速值,其采样周期为100ms。 我们接下来使用python脚本这些采样值进行可视化分析; import matplotlib.pyplot as plt import numpy as np from typing import Tuple, Dict, Any # ==================== 配置部分 ==================== def setup_matplotlib_config(): """设置matplotlib全局配置""" plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans'] plt.rcParams['axes.unicode_minus'] = False plt.rcParams['figure.autolayout'] = True # ==================== 数据准备部分 ==================== def generate_sample_data(sample_period: float = 0.01) -> Tuple[np.ndarray, np.ndarray]: """生成模拟数据用于测试""" # 模拟PID响应:从0到超调再到稳定 num_points = 300 # 生成更真实的PID响应曲线 time_array = np.arange(0, num_points * sample_period, sample_period) # 创建典型的二阶系统响应 t = time_array # 响应模型:快速上升 -> 超调 -> 衰减振荡 -> 稳定 speed_array = 1500 * (1 - np.exp(-3 * t)) # 指数上升 speed_array += 300 * np.sin(8 * t) * np.exp(-1.5 * t) # 衰减振荡 speed_array += np.random.normal(0, 20, len(t)) # 添加噪声 # 确保数据维度正确 return speed_array, time_array def load_real_data(file, sample_period: float = 0.01) -> Tuple[np.ndarray, np.ndarray]: """加载真实数据""" speed_array = np.loadtxt(file) time_array = np.arange(0, len(speed_array) * sample_period, sample_period) # 使用模拟生成的数据 # return generate_sample_data(sample_period) return speed_array, time_array # ==================== 分析计算部分 ==================== def calculate_pid_performance(speed_data: np.ndarray, target_rpm: float) -> Dict[str, Any]: """计算PID性能指标""" # 稳态分析(取后25%的数据) steady_start = int(len(speed_data) * 0.75) steady_data = speed_data[steady_start:] steady_mean = np.mean(steady_data) steady_std = np.std(steady_data) steady_error = steady_mean - target_rpm # 动态性能 max_value = np.max(speed_data) min_value = np.min(speed_data) overshoot = max(max_value - target_rpm, 0) undershoot = max(target_rpm - min_value, 0) # 上升时间(到达目标值90%的时间) ninety_percent = target_rpm * 0.9 if np.any(speed_data >= ninety_percent): rise_time_idx = np.where(speed_data >= ninety_percent)[0][0] else: rise_time_idx = 0 return { 'steady_mean': steady_mean, 'steady_std': steady_std, 'steady_error': steady_error, 'max_value': max_value, 'min_value': min_value, 'overshoot': overshoot, 'undershoot': undershoot, 'rise_time_idx': rise_time_idx, 'peak_time_idx': np.argmax(speed_data) if max_value > target_rpm else None } # ==================== 绘图部分 ==================== def create_pid_plot(speed_array: np.ndarray, time_array: np.ndarray, target_rpm: float = 1500) -> plt.Figure: """创建PID响应曲线图""" fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), gridspec_kw={'height_ratios': [3, 1]}) perf = calculate_pid_performance(speed_array, target_rpm) # ===== 主图:转速曲线 ===== # 绘制主曲线 ax1.plot(time_array, speed_array, 'b-', linewidth=2.5, label=f'实际转速 (n={len(speed_array)})', alpha=0.8) # 目标线和稳定带 ax1.axhline(y=target_rpm, color='r', linestyle='--', linewidth=2, label=f'目标: {target_rpm} RPM') stable_upper = target_rpm * 1.02 stable_lower = target_rpm * 0.98 ax1.fill_between(time_array, stable_lower, stable_upper, alpha=0.1, color='green', label='±2%稳定带') # ====== 修改这里:智能调整Y轴范围 ====== # 计算实际数据的范围 actual_min = np.min(speed_array) actual_max = np.max(speed_array) actual_range = actual_max - actual_min # 确保目标值在可视范围内(如果目标值很远) view_min = min(actual_min, target_rpm * 0.9) view_max = max(actual_max, target_rpm * 1.1) # 添加适当的边距(基于实际数据范围) if actual_range > 0: margin = actual_range * 0.15 # 15%的边距 else: margin = 100 # 默认边距 # 设置Y轴范围 ax1.set_ylim(view_min - margin, view_max + margin) # ===== 子图:误差曲线 ===== error_percent = (speed_array - target_rpm) / target_rpm * 100 ax2.plot(time_array, error_percent, 'gray', linewidth=1.5, alpha=0.7) ax2.fill_between(time_array, 0, error_percent, where=(error_percent >= 0), alpha=0.2, color='red') ax2.fill_between(time_array, 0, error_percent, where=(error_percent < 0), alpha=0.2, color='blue') ax2.axhline(y=0, color='black', linewidth=0.8, alpha=0.5) ax2.axhline(y=2, color='green', linestyle=':', alpha=0.5) ax2.axhline(y=-2, color='green', linestyle=':', alpha=0.5) ax2.set_xlabel('时间 (秒)', fontsize=12) ax2.set_ylabel('误差 (%)', fontsize=12) ax2.grid(True, alpha=0.2, linestyle=':') ax2.set_ylim(-10, 10) # ===== 添加统计信息 ===== stats_text = ( f'稳态均值: {perf["steady_mean"]:.1f} RPM\n' f'稳态误差: {perf["steady_error"]:.1f} RPM ({perf["steady_error"] / target_rpm * 100:.2f}%)\n' f'稳态波动: ±{perf["steady_std"]:.1f} RPM\n' f'超调量: {perf["overshoot"]:.1f} RPM ({perf["overshoot"] / target_rpm * 100:.1f}%)\n' f'调节范围: {perf["max_value"] - perf["min_value"]:.1f} RPM' ) fig.text(0.02, 0.02, stats_text, fontsize=10, bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.9)) plt.tight_layout() return fig # ==================== 报告输出部分 ==================== def print_performance_report(perf: Dict[str, Any], target_rpm: float): """打印性能分析报告""" print("=" * 60) print("PID控制性能分析报告".center(60)) print("=" * 60) metrics = [ ("目标转速", f"{target_rpm} RPM"), ("稳态转速", f"{perf['steady_mean']:.1f} RPM"), ("稳态误差", f"{perf['steady_error']:.1f} RPM ({perf['steady_error'] / target_rpm * 100:.2f}%)"), ("稳态标准差", f"{perf['steady_std']:.1f} RPM"), ("最大转速", f"{perf['max_value']:.1f} RPM"), ("最小转速", f"{perf['min_value']:.1f} RPM"), ("超调量", f"{perf['overshoot']:.1f} RPM ({perf['overshoot'] / target_rpm * 100:.2f}%)"), ("调节范围", f"{perf['max_value'] - perf['min_value']:.1f} RPM"), ("控制精度", f"{perf['steady_std'] / target_rpm * 100:.2f}%") ] for label, value in metrics: print(f"{label:>15}: {value}") print("=" * 60) # 性能评估 if abs(perf['steady_error']) < target_rpm * 0.01: print("✓ 稳态精度优秀 (<1%)") elif abs(perf['steady_error']) < target_rpm * 0.02: print("✓ 稳态精度良好 (<2%)") else: print("⚠ 稳态精度有待提高") if perf['overshoot'] < target_rpm * 0.1: print("✓ 超调控制优秀 (<5%)") elif perf['overshoot'] < target_rpm * 0.1: print("✓ 超调控制良好 (<10%)") if perf['steady_std'] < target_rpm * 0.01: print("✓ 稳定性优秀 (波动<1%)") # ==================== 主程序 ==================== def main(): """主程序""" # 1. 初始化配置 setup_matplotlib_config() # 2. 加载数据(替换为您的真实数据) speed_array, time_array = load_real_data('speed_data.txt', 0.1) print(f"数据加载完成: {len(speed_array)} 个点,时长 {time_array[-1]:.1f} 秒") # 3. 创建图形 target_rpm = 1500 fig = create_pid_plot(speed_array, time_array, target_rpm) # 4. 保存图形 fig.savefig('pid_response_analysis.png', dpi=300, bbox_inches='tight') print(f"\n图表已保存: pid_response_analysis.png") # 5. 性能分析和报告 perf = calculate_pid_performance(speed_array, target_rpm) print_performance_report(perf, target_rpm) # 6. 显示图形 plt.show() # ==================== 程序入口 ==================== if __name__ == "__main__": main() 2.3.1 调整P 最初我将P设置为0.01,输出的占空比为15%(由PID算法计算得到),由于我将编码器焊接到电机上后,增加了摩擦阻力,此时电机根本不会转动。 因此我们先将P值设置为0.02,然后烧录程序,并将转速的采样值保存到speed_data.txt文件; P I D 0.02 0 0 运行python分析脚本,看看效果,从速度曲线可以看出,达不到目标速度,且与目标速度相差较大,稳态误差大概在788 rpm; P值加大到0.03,从速度曲线可以看出,与目标转速减小,稳态误差大概在556 rpm; P I D 0.03 0 0 P值加大到0.04,从速度曲线可以看出,与目标转速进一步减小,稳态误差大概在116 rpm,但是出现了超调; P I D 0.04 0 0 P值加大到0.05,从速度曲线可以看出,与目标转速增大,稳态误差变大,大概在128 rpm,并且出现了震荡; P I D 0.05 0 0 只使用P,会存在静差,始终达到不了目标值,这时就要使用积分项来消除静差了。 2.3.2 调整I P保持0.04,I使用0.004,从速度曲线可以看出,可以达到目标速度。 P I D 0.04 0.004 0 P保持0.04,加大I,使用0.008,从速度曲线可以看出,同样可以达到目标速度。 P保持0.04,加大I,使用0.01,从速度曲线可以看出,同样可以达到目标速度。 比较上面三个速度曲线,我们发现跟踪的速度均是很快的,I=0.004时稳态波动最小。 对于过冲,可以再加入微分试试,微分D相当于阻力的效果。 2.3.3 调整D P保持0.04,I保持0.004,D使用0.002,从速度曲线上,出现了震荡。 P I D 0.04 0.004 0.002 P保持0.04,I保持0.004,减小D,D使用0.0002,从速度曲线上,和PI控制相比好像看不出明显的变化。 因此可以考虑只用PI,不要D,此外在实际项目中,一定要选择测量精度较高的光电编码器。 三、源码下载 源码下载路径:stm32f103。