从立创天猛星到地阔星:基于MSPM0G3507与STM32F103的PID电机控制项目复刻与移植实战
从立创天猛星到地阔星:基于MSPM0G3507与STM32F103的PID电机控制项目复刻与移植实战
最近有不少朋友在复刻立创开源平台上的那个简易PID电机控制项目,过程中遇到了不少问题,尤其是想把项目从TI的MSPM0G3507(天猛星开发板)移植到ST的STM32F103C8T6(地阔星开发板)上时,感觉无从下手。我自己也完整走了一遍这个流程,今天就把我的复刻和移植经验分享出来,希望能帮你少踩几个坑。
这个项目本身是一个非常好的PID算法学习案例,它用一块1.9寸SPI屏幕做交互,五个独立按键操作,通过编码器反馈,实现了电机的定速和定距控制。咱们今天不光要把它跑起来,还要搞明白怎么把它的代码从一套硬件平台搬到另一套上,这对于理解嵌入式开发的硬件抽象和分层设计非常有帮助。
1. 项目整体认识:硬件与软件架构
在动手之前,咱们先搞清楚这个项目到底在做什么,以及它的代码是怎么组织的。这样移植的时候才知道从哪里下手。
1.1 项目是干什么的?
简单说,就是用一个单片机(MCU)去精确控制一个直流电机的转动。怎么个精确法呢?有两种模式:
- 定速模式:让电机按照你设定的转速(比如每分钟100转)稳定运行,不管负载怎么轻微变化,它都能保持住。
- 定距模式:让电机精确转动你设定的角度或圈数(比如转动180度),然后停下来。
为了实现这种精确控制,项目使用了经典的PID控制算法。PID就像一个聪明的“调节器”,它会根据电机的实际状态(通过编码器测量)和你期望的目标状态之间的“误差”,自动计算出应该给电机多大的“劲儿”(PWM占空比)。
1.2 硬件平台对比
项目涉及两块开发板,咱们先看看它们的核心区别:
| 特性 | 立创·天猛星 (MSPM0G3507) | 立创·地阔星 (STM32F103C8T6) |
|---|---|---|
| 核心MCU | TI MSPM0G3507 (ARM Cortex-M0+) | ST STM32F103C8T6 (ARM Cortex-M3) |
| 开发库 | TI DriverLib (DL库) | ST HAL库 |
| PWM引脚 | GPIOA26 (TIMG7-CH0), GPIOA27 (TIMG7-CH1) | PA3 (TIM2-CH4), PA6 (TIM3-CH1) |
| 编码器引脚 | GPIOB0 (A相), GPIOB1 (B相) | PB0 (A相), PB1 (B相) |
| SPI屏幕引脚 | 未在原文详述 | PB13(SCK), PB15(MOSI), PA2(RES), PA0(DC), PB12(CS), PA1(BLK) |
| 按键引脚 | PA8, PA9, PA28, PA31, PB4 | PB3, PB4, PB5, PB6, PB7 |
| 串口引脚 | 未在原文详述 | PA9(TX), PA10(RX) |
注意:硬件引脚是移植时需要修改的核心部分,必须根据你的实际电路连接来配置。
1.3 软件架构:三层设计是关键
这个项目的代码结构非常清晰,采用了典型的分层设计,这也是它能顺利移植的基础。咱们来拆解一下:
项目根目录/ ├── app/ (应用层) │ ├── app_sys_mode.c // 系统状态机、页面管理 │ ├── app_speed_pid.c // 定速PID控制逻辑 │ ├── app_distance_pid.c // 定距PID控制逻辑 │ ├── app_key_task.c // 按键事件处理 │ └── app_ui.c // 屏幕显示、UI绘制 ├── middle/ (中间层) │ ├── mid_pid.c // 核心PID算法(与硬件无关!) │ ├── mid_timer.c // 定时器任务调度 │ ├── mid_button.c // 按键扫描、消抖逻辑 │ ├── mid_debug_led.c // 调试LED │ └── mid_debug_uart.c // 调试串口打印 └── hardware/ (硬件层) ├── hw_motor.c // 电机PWM驱动 ├── hw_encoder.c // 编码器数据读取 ├── hw_lcd.c // SPI屏幕驱动 ├── hw_key.c // 按键GPIO读取 ├── lcdfont.h // 字库 └── pic.h // 图片资源分层的好处:
- 硬件层 (hardware):只负责和具体的芯片外设(GPIO、TIMER、SPI等)打交道。移植时,主要修改的就是这一层。
- 中间层 (middle):提供通用的软件服务,比如PID计算、定时器管理。这部分代码几乎不用改,因为它不依赖具体硬件。
- 应用层 (app):实现具体的业务逻辑,比如选择控制模式、处理用户按键。这部分代码完全不用改,它只调用中间层和硬件层提供的接口。
这种设计让移植工作变得模块化:你只需要为新的芯片平台重新实现hardware层,上层的业务逻辑就能无缝运行。
2. 核心模块原理与代码解析
理解了架构,咱们再深入看看几个核心模块是怎么工作的。知其然,更要知其所以然。
2.1 PID算法:控制器的“大脑”
PID算法的核心思想就是根据“误差”来调整输出。误差 = 目标值 - 当前值。PID控制器由三部分组成:
- P (比例):误差越大,输出调整力度越大。反应快,但单独使用容易震荡或存在静差。
- I (积分):累积历史误差。能消除静差,但反应慢,积分太强会超调。
- D (微分):预测误差变化趋势。能抑制震荡,让系统更稳定。
项目中的PID实现(mid_pid.c)非常标准,咱们来看关键函数:
float pid_calc(PID *pid, float target, float current) { // 1. 计算本次误差,并保存上一次误差(用于微分) pid->last_error = pid->error; pid->error = target - current; // 2. 计算P、I、D三个分量 float pout = pid->error; // 比例项 = 当前误差 pid->change_i += pid->error; // 积分项 = 误差累积和 float dout = pid->error - pid->last_error; // 微分项 = 误差的变化率 // 3. 积分限幅:防止积分项无限增大(积分饱和) if(pid->change_i > pid->max_change_i) pid->change_i = pid->max_change_i; else if(pid->change_i < -pid->max_change_i) pid->change_i = -pid->max_change_i; // 4. 计算最终PID输出:Output = Kp*P + Ki*I + Kd*D pid->output = (pid->kp * pout) + (pid->ki * pid->change_i) + (pid->kd * dout); // 5. 输出限幅:防止输出超过执行器(电机)能接受的范围 if(pid->output > pid->max_output) pid->output = pid->max_output; else if(pid->output < -pid->max_output) pid->output = -pid->max_output; return pid->output; }提示:
kp,ki,kd这三个参数需要根据你的具体电机和负载来调试,没有万能值。调试顺序一般是先调P,让系统有基本响应;再加I消除静差;最后加D抑制震荡。
2.2 编码器:电机的“眼睛”
编码器是测量电机转速和位置的关键传感器。本项目用的是双相正交编码器,电机转一圈,它会输出固定数量的脉冲(比如1000个)。通过检测A、B两相信号的相位差,还能判断电机的正反转。
编码器的读数在中断里实时更新(hw_encoder.c),速度计算则在定时器中断里完成:
// 在20ms的定时器中断中调用此函数 void encoder_update_speed(void) { static uint32_t last_count = 0; uint32_t current_count = encoder_get_count(); // 获取当前总脉冲数 // 计算过去20ms内脉冲数的增量 int32_t delta_count = current_count - last_count; // 将脉冲增量转换为转速(RPM,转/分钟) // 假设编码器分辨率是1000脉冲/转 // delta_count / 1000 = 转数 // (转数 / 0.02秒) * 60秒 = RPM float speed_rpm = (delta_count * 50.0 * 60.0) / 1000.0; encoder_data.speed = speed_rpm; // 更新速度值 last_count = current_count; // 保存本次计数,用于下次计算 }2.3 电机驱动:控制器的“手”
电机驱动芯片(BDR6126D)接收单片机发出的PWM信号,来控制电机的电压和方向。项目中用两个PWM通道(BI和FI)来控制电机的正反转和停止,这是一种典型的H桥控制思路。
void hw_motor_set(int16_t pwm_value) { if(pwm_value > 0) // 正转 { // BI引脚输出PWM,FI引脚输出低电平 set_bi(pwm_value); // 对应原文的 DL_TimerG_setCaptureCompareValue set_fi(0); } else if(pwm_value < 0) // 反转 { // BI引脚输出低电平,FI引脚输出PWM set_bi(0); set_fi(-pwm_value); // 注意取绝对值 } else // 停止 { // 两个引脚都输出低电平,电机两端电压为0 set_bi(0); set_fi(0); } }3. 从MSPM0G3507到STM32F103的移植实战
好了,理论基础打好了,现在进入最关键的实战环节:如何把项目从天猛星(MSPM0)搬到地阔星(STM32)上。
3.1 移植第一步:引脚重映射
这是最直接的一步。你需要根据“地阔星”开发板的原理图和你自己的连接方式,重新定义每个功能对应的GPIO引脚。
打开你的STM32工程(使用STM32CubeMX生成初始化代码最方便),根据之前对比表格里的信息,配置以下引脚:
- PWM输出:配置TIM2的通道4(PA3)和TIM3的通道1(PA6)为PWM Generation模式。
- 编码器输入:配置PB0和PB1为GPIO_Input模式,并开启这两个引脚的外部中断(EXTI)。
- SPI屏幕:配置SPI2(PB13为SCK,PB15为MOSI),其他控制引脚(RES, DC, CS, BLK)配置为GPIO_Output。
- 按键:配置PB3, PB4, PB5, PB6, PB7为GPIO_Input模式,通常需要启用内部上拉电阻。
- 调试LED/串口:按需配置。
3.2 硬件驱动层(hardware)的改写
这是移植的核心工作,需要用STM32 HAL库的函数,替换掉原项目中TI DriverLib的函数。
1. SPI屏幕驱动 (hw_lcd.c)原项目可能用GPIO模拟SPI或芯片自带SPI,移植到STM32时,我们使用硬件SPI。关键改动在于数据写入函数:
// 原MSPM0代码(可能是GPIO模拟或专用函数) void spi_write_bus(unsigned char dat) { // ... 原有实现 ... } // 移植后的STM32 HAL库版本 void spi_write_bus(unsigned char dat) { // 使用HAL库的阻塞式SPI发送函数 HAL_SPI_Transmit(&hspi2, &dat, 1, HAL_MAX_DELAY); }同时,屏幕的其他控制引脚(如CS、DC、RESET)的宏定义,也要从直接操作寄存器改为HAL库的GPIO操作函数:
// 例如,原代码可能是 DL_GPIO_writePin,移植后改为: #define LCD_CS_Clr() HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_RESET) #define LCD_CS_Set() HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_SET)2. 按键扫描 (hw_key.c)按键读取从TI的DL_GPIO_readPins改为HAL库的HAL_GPIO_ReadPin。
KEY_STATUS key_scan(void) { KEY_STATUS states; states.up = HAL_GPIO_ReadPin(GPIOB, KEY_UP_Pin) ? 1 : 0; states.down = HAL_GPIO_ReadPin(GPIOB, KEY_DOWN_Pin) ? 1 : 0; // ... 读取其他按键 return states; }3. 电机PWM控制 (hw_motor.c)设置PWM占空比的函数需要替换。原项目使用DL_TimerG_setCaptureCompareValue,STM32 HAL库使用__HAL_TIM_SetCompare。
// 设置FI引脚(对应TIM2_CH4)的PWM static void set_fi(uint16_t compare_value) { __HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_4, compare_value); } // 设置BI引脚(对应TIM3_CH1)的PWM static void set_bi(uint16_t compare_value) { __HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_1, compare_value); }切记:在main函数初始化阶段,需要先启动PWM输出:
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_4); HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);4. 编码器驱动 (hw_encoder.c)编码器使用外部中断检测脉冲。原项目的中断服务函数(ISR)名称是GROUP1_IRQHandler,在STM32中,我们需要在HAL库提供的通用回调函数HAL_GPIO_EXTI_Callback里处理。
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if(GPIO_Pin == ENCODER_A_Pin) { // 判断B相电平,确定方向 if(HAL_GPIO_ReadPin(ENCODER_B_GPIO_Port, ENCODER_B_Pin) == GPIO_PIN_RESET) { motor_encoder.temp_count++; // 正转 } else { motor_encoder.temp_count--; // 反转 } } // 同样处理B相中断(如果需要双边沿检测) }5. 定时器 (mid_timer.c)原项目的定时器中断服务函数是TIMER_LED_INST_IRQHandler,在STM32中,我们使用HAL库的更新中断回调函数。
// 启动定时器中断(在main初始化中调用) void timer_init(void) { HAL_TIM_Base_Start_IT(&htim1); // 假设使用TIM1做系统定时器 } // 定时器更新中断回调函数 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim == &htim1) { // 判断是哪个定时器触发的 // 执行20ms任务:更新编码器速度、扫描按键 if(get_functional_mode() != DISTANCE_FUNCTION) { encoder_update(); // 更新速度 } if(get_task_status() == TASK_ENABLE) { flex_button_scan(); // 按键扫描 } } }3.3 中间层与应用层:基本无需改动
这就是分层设计带来的巨大优势!middle/目录下的mid_pid.c(PID算法)、mid_button.c(按键状态机)等代码,完全不依赖硬件,直接复制过来就能用。app/目录下的所有业务逻辑代码,更是原封不动地搬过来。
你只需要确保hardware层提供的函数接口(如hw_motor_set,encoder_get_count,key_scan)名称和功能不变,上层代码就能正常调用。
3.4 编译环境配置:一个容易忽略的大坑
如果你用的是Keil MDK开发STM32F103,这里有个超级大坑:Keil MDK默认安装的是ARM Compiler 6 (AC6),但很多STM32F1的旧项目或库是基于ARM Compiler 5 (AC5) 编写的,直接编译会报一堆错。
解决方法:
- 安装AC5:你需要单独下载并安装ARM Compiler 5。安装时,务必将其安装到Keil的安装目录下的
ARM文件夹里(例如C:\Keil_v5\ARM\ARMCC)。 - 项目配置:在Keil中打开你的STM32项目,点击魔术棒按钮 ->
Target选项卡,在Code Generation区域,将Use default compiler version 5改为Use default compiler version 5。如果下拉列表里没有,可能需要手动选择你安装的AC5路径。 - 重新激活:更换编译器后,Keil可能需要你重新完成许可证激活步骤。
4. 二次开发与调试心得
移植成功后,你还可以在原项目基础上做一些个性化修改,比如原文作者就做了:
- 增加OK键:将原项目的右键功能赋予一个新的OK键,使操作更符合直觉。
- 丰富UI界面:在首页增加了显示嘉立创Logo和常用网址的页面。
- 添加系统运行指示灯:用一个LED闪烁指示系统正在运行。
在调试你自己的项目时,我有几个小建议:
- 先调通硬件:确保屏幕能亮、按键能读、电机能转、编码器有计数,再谈PID。
- PID参数从零开始:先把
ki和kd设为0,只调kp。慢慢增大kp,直到电机出现等幅振荡,然后取这个值的60%作为初始kp。 - 善用串口打印:把目标速度、实际速度、PID输出等关键数据通过串口打印出来,用电脑上的串口助手软件看波形,比盲目猜测高效得多。
这个从“天猛星”到“地阔星”的移植过程,本质上是一次非常好的学习经历。它强迫你去理解每一行代码背后的硬件操作,去思考如何将业务逻辑与硬件细节解耦。当你成功让电机在另一块板子上精准转动起来的时候,你对嵌入式系统分层设计的理解,绝对会上一个台阶。
