简介这份资源面向正在学习TI MSPM0G3507微控制器的嵌入式开发者与电赛备赛学生提供一套可直接编译运行的简易PID项目完整代码帮助读者快速理解PID算法在MSPM0G3507平台上的落地方式并可作为电机控制、编码器测速等场景的工程模板。压缩包共56个文件约158KB以15个C源文件与13个头文件为核心涵盖电机驱动、编码器、按键、定时器、LCD与OLED显示、串口通信及系统模式管理等模块同时附带IAR、Keil、TI Clang与GCC多套工程配置以及SysConfig配置文件、链接脚本、启动文件和README说明方便在不同工具链下直接导入使用。目前已有158人学习下载。代码结构按底层驱动与应用逻辑分层组织读者可据此掌握外设初始化、任务调度与PID闭环调节的完整实现思路并借助现成工程快速验证与二次开发。1. 从零搭建 MSPM0G3507 的 PID 控制为什么你的电机一上电就抖很多人第一次在 MSPM0G3507 上跑 PID都会遇到同一个场景代码烧进去电机一上电就嗡嗡抖或者转速像过山车一样忽高忽低。你以为是参数没调好反复改 Kp、Ki结果越改越乱。问题往往不在 PID 本身而在于你对这颗芯片的定时器、ADC 采样和 PWM 输出这三条链路的理解有偏差。MSPM0G3507 是 TI 的 Arm Cortex-M0 平台主频 80MHz带硬件乘法器跑 PID 完全够用但它的外设配置逻辑和 STM32 那套习惯差别不小。这篇笔记面向的是手里已经有一块 MSPM0G3507 开发板、想用增量式或位置式 PID 控制直流电机或温控模块的嵌入式开发者。我会把完整代码拆成可复现的步骤从定时器中断周期怎么定、ADC 采样怎么和 PWM 对齐到 PID 输出限幅和积分抗饱和每一步都给出参数依据和翻车点。读完你能拿到一套可以直接移植的 PID 框架而不是一堆需要猜的寄存器配置。2. 先搞清楚 MSPM0G3507 跑 PID 的硬件底子2.1 为什么定时器中断周期决定了 PID 的生死PID 计算必须在一个严格固定的时间间隔里执行这个间隔就是采样周期 Ts。MSPM0G3507 的通用定时器 TIMG 可以配置成周期中断模式我一般把 Ts 定在 1ms 到 10ms 之间。Ts 太大积分项累积慢系统响应迟钝Ts 太小M0 内核频繁进出中断CPU 开销上去而且 ADC 采样还没稳定就被读走噪声直接灌进微分项。具体怎么算假设你用 TIMG0总线时钟 40MHz预分频设为 40则计数时钟 1MHz装载值设为 999中断周期就是 (9991)/1MHz 1ms。这个 1ms 不是随便定的它要和你电机的电气时间常数匹配。小型直流电机的电气时间常数通常在 0.5ms 到 5ms机械时间常数在 10ms 到 100ms所以 1ms 采样对电流环够用对速度环偏快但可以接受。配置代码如下注意 MSPM0 的 DriverLib 函数名和 STM32 HAL 完全不同#include ti_msp_dl_config.h void TIMG0_Init(void) { // 使能 TIMG0 电源 DL_TimerG_reset(TIMG0); DL_TimerG_enablePower(TIMG0); // 配置为周期中断模式总线时钟 40MHz预分频 40 - 1MHz DL_TimerG_setClockConfig(TIMG0, (DL_TimerG_ClockConfig *) gTIMG0ClockConfig); DL_TimerG_initTimerMode(TIMG0, (DL_TimerG_TimerConfig *) gTIMG0TimerConfig); // 装载值 999周期 1ms DL_TimerG_setLoadValue(TIMG0, 999); DL_TimerG_enableInterrupt(TIMG0, DL_TIMERG_INTERRUPT_ZERO_EVENT); DL_TimerG_enableClock(TIMG0); // 使能 NVIC 中断 NVIC_EnableIRQ(TIMG0_INT_IRQn); }这段代码里gTIMG0ClockConfig和gTIMG0TimerConfig是 SysConfig 生成的全局结构体如果你用纯寄存器写法需要手动填CLKCFG和CTL寄存器。关键参数是预分频值prescale和装载值loadValue两者共同决定中断频率。逻辑说明DL_TimerG_setLoadValue设置的是向下计数到零的 reload 值所以实际周期是 (loadValue1) 个计数时钟。参数说明预分频 40 对应 40MHz/401MHz装载值 999 对应 1000 个计数合起来 1ms。如果你把预分频改成 20计数时钟变 2MHz同样装载值周期就变 0.5msPID 参数需要重新整定。2.2 ADC 采样怎么和 PWM 对齐才不引入纹波电机电流或转速反馈通常来自 ADC。MSPM0G3507 的 ADC12 模块支持硬件触发可以由定时器事件直接启动转换这样采样时刻和 PWM 周期严格同步避免在 PWM 开关瞬间采到尖峰。我一般把 ADC 触发源设为 TIMG0 的零事件也就是和 PID 中断同一个时刻。但这里有个坑ADC 转换需要时间12 位精度下采样加转换大概 1us 到 2us如果你在中断里立刻读结果可能读到的是上一次的旧值。正确做法是在中断里启动 ADC等转换完成中断或轮询 EOC 标志后再读或者把 ADC 触发提前到 PWM 周期中间让采样点落在电流平稳区。void ADC12_Init(void) { DL_ADC12_reset(ADC12_0_INST); DL_ADC12_enablePower(ADC12_0_INST); // 配置为定时器触发单次转换 DL_ADC12_initSingleSample(ADC12_0_INST, DL_ADC12_REPEAT_MODE_DISABLED, DL_ADC12_SAMPLING_SOURCE_AUTO, DL_ADC12_TRIG_SRC_EVENT, DL_ADC12_TRIG_SRC_EVENT, DL_ADC12_SAMP_CONV_RES_12_BIT, DL_ADC12_SAMP_CONV_DATA_FORMAT_UNSIGNED); // 设置采样时间约 1us DL_ADC12_setSampleTime(ADC12_0_INST, 40); DL_ADC12_enableConversions(ADC12_0_INST); }逻辑说明DL_ADC12_TRIG_SRC_EVENT表示由硬件事件触发事件源在 SysConfig 里绑定到 TIMG0 的零事件。参数说明采样时间 40 个 ADC 时钟假设 ADC 时钟 40MHz就是 1us。这个值不能太小否则采样保持电容充不满读数偏低也不能太大否则转换周期拉长影响控制频率。我一般会先用示波器看采样点是否落在 PWM 低电平中间再微调采样时间。2.3 PWM 输出的死区和频率怎么选MSPM0G3507 的 TIMG 可以输出带死区的互补 PWM适合驱动 H 桥。PWM 频率我一般选 20kHz 到 40kHz低于 20kHz 人耳能听到啸叫高于 40kHz 开关损耗上去。死区时间根据你的栅极驱动芯片和 MOS 管开关速度定通常 200ns 到 1us。配置时注意 MSPM0 的死区寄存器是 8 位单位是计数时钟周期。假设计数时钟 40MHz死区 200ns 对应 8 个计数。PWM 占空比更新最好放在 PID 中断里和 ADC 采样错开半个周期这样占空比变化不会立刻影响刚采到的反馈值。void PWM_Init(void) { DL_TimerG_reset(PWM_0_INST); DL_TimerG_enablePower(PWM_0_INST); // 配置为 PWM 模式频率 20kHz DL_TimerG_initPWMMode(PWM_0_INST, (DL_TimerG_PWMConfig *) gPWM_0Config); // 死区 200ns计数时钟 40MHz - 8 个计数 DL_TimerG_setDeadZone(PWM_0_INST, 8); DL_TimerG_enableClock(PWM_0_INST); }逻辑说明DL_TimerG_initPWMMode会配置计数模式、装载值和比较值。参数说明20kHz 对应周期 50us如果计数时钟 40MHz装载值就是 2000。死区 8 个计数对应 200ns。这里要注意死区太大会导致输出电压失真太小会直通炸管我一般先用示波器看上下管驱动波形确认死区期间两个管子都关断再上电。3. 位置式 PID 的 C 代码实现与参数整定3.1 位置式 PID 的完整结构体和计算函数位置式 PID 的输出直接对应执行器位置适合温控、位置伺服这类场景。在 MSPM0G3507 上我用一个结构体保存所有状态避免全局变量散落。核心计算函数放在 1ms 中断里调用输入是设定值和反馈值输出是 PWM 比较值。注意 M0 没有浮点单元浮点运算靠软件模拟一次乘加大概几十个周期1ms 中断里跑单路 PID 绰绰有余但如果你要跑多路建议改成定点数。typedef struct { float Kp; // 比例系数 float Ki; // 积分系数 float Kd; // 微分系数 float integral; // 积分累积 float prev_error; // 上一次误差 float out_max; // 输出上限 float out_min; // 输出下限 } PID_TypeDef; float PID_Compute(PID_TypeDef *pid, float setpoint, float feedback) { float error setpoint - feedback; // 比例项 float p_term pid-Kp * error; // 积分项带抗饱和 pid-integral pid-Ki * error; if (pid-integral pid-out_max) pid-integral pid-out_max; if (pid-integral pid-out_min) pid-integral pid-out_min; // 微分项用差分代替微分 float d_term pid-Kd * (error - pid-prev_error); pid-prev_error error; // 求和并限幅 float output p_term pid-integral d_term; if (output pid-out_max) output pid-out_max; if (output pid-out_min) output pid-out_min; return output; }逻辑说明积分项先累加再限幅这叫积分抗饱和防止设定值突变时积分项无限增长。微分项用后向差分prev_error每次更新。参数说明out_max和out_min对应 PWM 占空比的上下限比如 0 到 2000。Kp、Ki、Kd的整定顺序是先 P 后 I 再 D具体方法下一节讲。3.2 中断服务函数里怎么调用 PID 并更新 PWM中断服务函数要短只做采样、计算、更新三步。ADC 读取用轮询 EOC 标志因为转换在中断触发后 1us 就完成了轮询比再开一个中断更简单。PWM 更新用DL_TimerG_setCaptureCompareValue注意这个函数写的是比较值不是占空比百分比。void TIMG0_IRQHandler(void) { if (DL_TimerG_getPendingInterrupt(TIMG0) DL_TIMERG_IIDX_ZERO) { // 1. 读取 ADC 反馈值 while (!DL_ADC12_isConversionComplete(ADC12_0_INST)); uint16_t adc_raw DL_ADC12_getMemResult(ADC12_0_INST, DL_ADC12_MEM_IDX_0); float feedback (float)adc_raw * 3.3f / 4095.0f; // 2. 计算 PID float output PID_Compute(g_pid, g_setpoint, feedback); // 3. 更新 PWM 比较值 uint32_t compare (uint32_t)(output); DL_TimerG_setCaptureCompareValue(PWM_0_INST, compare, DL_TIMER_CC_0_INDEX); DL_TimerG_clearInterruptStatus(TIMG0, DL_TIMERG_INTERRUPT_ZERO_EVENT); } }逻辑说明DL_ADC12_isConversionComplete等待转换完成DL_ADC12_getMemResult读结果。反馈值换算成电压如果你用的是电流采样电阻还需要除以增益和电阻值得到实际电流。参数说明g_setpoint是目标值compare直接写比较寄存器范围 0 到装载值。注意DL_TimerG_setCaptureCompareValue的第三个参数是通道索引别写错。3.3 参数整定的三步法和一个实用技巧整定顺序先只加 Kp从小到大加直到系统开始等幅振荡记下此时的 Kp 为 Ku振荡周期为 Tu。然后按 Ziegler-Nichols 经验公式Kp0.6KuKi2Kp/TuKdKp*Tu/8。但实际电机系统往往不需要这么激进我一般把 Kp 取 0.4KuKi 取 Kp/TuKd 取 0。为什么 Kd 取 0因为 M0 的 ADC 噪声大微分项会把噪声放大除非你加了硬件滤波或滑动平均。一个实用技巧在中断里对反馈值做一阶低通滤波滤波系数 0.1 到 0.3能明显压住微分噪声。// 一阶低通滤波alpha 越小滤波越强 float feedback_filtered 0.0f; float alpha 0.2f; feedback_filtered alpha * feedback (1.0f - alpha) * feedback_filtered;逻辑说明每次中断更新滤波值用滤波后的值参与 PID 计算。参数说明alpha 取 0.2 表示新值占 20%旧值占 80%截止频率大概几十赫兹对 1ms 采样周期合适。如果 alpha 太小系统响应变慢相位滞后增加可能引起振荡。4. 增量式 PID 在电机速度环上的移植与对比4.1 增量式 PID 的公式推导和代码差异增量式 PID 输出的是控制量的增量不是绝对位置适合带积分环节的执行器比如电机速度环。公式是 Δu Kp*(e(k)-e(k-1)) Kie(k) Kd(e(k)-2e(k-1)e(k-2))。和位置式相比增量式不需要累加积分天然抗饱和而且切换手动自动时冲击小。在 MSPM0G3507 上增量式代码更省 RAM因为只需要保存前两次误差。typedef struct { float Kp; float Ki; float Kd; float prev_error1; float prev_error2; float out_max; float out_min; } PID_IncTypeDef; float PID_IncCompute(PID_IncTypeDef *pid, float setpoint, float feedback) { float error setpoint - feedback; float delta pid-Kp * (error - pid-prev_error1) pid-Ki * error pid-Kd * (error - 2.0f * pid-prev_error1 pid-prev_error2); pid-prev_error2 pid-prev_error1; pid-prev_error1 error; // 增量限幅 if (delta pid-out_max) delta pid-out_max; if (delta pid-out_min) delta pid-out_min; return delta; }逻辑说明delta是本次控制量的增量需要在外层累加到实际输出上。参数说明out_max和out_min限制的是单次增量不是总输出这个值一般取总输出范围的 10% 到 20%。prev_error1和prev_error2每次更新注意顺序不能反。4.2 速度环里增量式怎么和 PWM 累加器配合增量式输出需要累加我一般用一个全局变量pwm_duty保存当前占空比每次中断加上 delta再限幅到 0 到装载值。这样即使 PID 输出有误占空比也不会突变。static float pwm_duty 0.0f; void SpeedLoop_ISR(void) { float feedback Get_Speed_Feedback(); float delta PID_IncCompute(g_pid_inc, g_target_speed, feedback); pwm_duty delta; if (pwm_duty 2000.0f) pwm_duty 2000.0f; if (pwm_duty 0.0f) pwm_duty 0.0f; DL_TimerG_setCaptureCompareValue(PWM_0_INST, (uint32_t)pwm_duty, DL_TIMER_CC_0_INDEX); }逻辑说明pwm_duty是累加器delta是增量。参数说明限幅值 2000 对应 20kHz PWM 的满占空比。注意pwm_duty要用静态变量或全局变量不能是局部变量否则每次中断都从零开始。4.3 位置式和增量式的选型对比对比项位置式 PID增量式 PID输出含义绝对控制量控制量增量积分饱和需要额外抗饱和天然无饱和内存占用需要保存积分项只需保存前两次误差切换冲击较大较小适用场景温控、位置伺服速度环、流量控制MSPM0G3507 上的 CPU 开销略高略低选型建议如果你的执行器是阀门、加热器这类需要绝对位置的用位置式如果是电机速度、风扇转速这类带积分环节的用增量式。我在某图像处理Demo的云台控制里用的是位置式因为云台角度需要绝对定位在散热风扇调速里用的是增量式因为风扇本身有惯性增量式更平滑。5. 避坑与排查MSPM0G3507 跑 PID 的五个血泪教训5.1 电机一上电就满速转PID 完全失控现象烧录后电机直接满速调什么参数都没用。原因PWM 初始占空比没有设为零或者 PID 输出限幅的out_min设成了负数而 PWM 比较值是无符号数负数被转成大正数。解决在初始化时显式调用DL_TimerG_setCaptureCompareValue(PWM_0_INST, 0, DL_TIMER_CC_0_INDEX)并且把out_min设为 0。另外检查 ADC 反馈是否接反如果反馈值始终为 0PID 会以为电机没转输出一直加到最大。5.2 转速周期性波动示波器看 PWM 占空比在抖现象电机转速围绕目标值来回摆周期大概几十毫秒。原因微分项对 ADC 噪声敏感或者采样周期和 PWM 周期不同步导致每次采到的反馈值相位不同。解决先加一阶低通滤波alpha 取 0.2然后把 ADC 触发源改成 PWM 周期中间点避开开关噪声。如果还抖把 Kd 设为 0只用 PI 控制。5.3 积分项越加越大电机响应越来越慢现象设定值突变后电机慢慢悠悠才跟上而且超调很大。原因积分抗饱和没做或者out_max设得太大积分项累积到很大值才被限幅。解决在积分累加后立刻限幅限幅值等于out_max和out_min。另外可以在误差符号反转时清零积分项这叫积分分离能加快大误差时的响应。5.4 中断里读 ADC 读到旧值反馈滞后一个周期现象PID 响应总是慢一拍感觉像延迟。原因ADC 转换还没完成就读结果读到的是上一次的转换值。解决在中断里用while(!DL_ADC12_isConversionComplete(ADC12_0_INST));等待完成或者把 ADC 触发提前到中断前 500us让转换在中断到来时刚好完成。注意等待循环要有超时退出防止死机。5.5 浮点运算导致中断超时系统卡死现象PID 中断里浮点运算太多1ms 中断实际执行了 1.5ms主循环完全没时间跑。原因M0 没有 FPU浮点乘加靠软件模拟一次大概 50 到 100 个周期。解决把 PID 参数和反馈值改成 Q15 定点数用整数乘加代替浮点。或者把 PID 计算频率降到 2ms 一次给 CPU 留余量。我一般会在中断里翻转一个 GPIO用示波器看中断执行时间超过 50% 周期就要优化。6. 进阶技巧用 SysConfig 生成代码后手动改哪几个寄存器SysConfig 是 TI 的图形化配置工具能生成 MSPM0G3507 的初始化代码但生成的代码往往偏保守比如中断优先级默认都是 0PWM 死区默认关闭。我一般会在生成代码后手动改三个地方第一把 PID 中断的优先级设成最高避免被其他中断打断导致采样周期抖动调用NVIC_SetPriority(TIMG0_INT_IRQn, 0);。第二打开 PWM 死区在 SysConfig 里可能没暴露这个选项需要手动写DL_TimerG_setDeadZone(PWM_0_INST, 8);。第三把 ADC 的采样时间从默认值改小默认可能给 100 个时钟转换太慢改成 40 个时钟。还有一个技巧在中断里用 DMA 把 ADC 结果直接搬到内存CPU 不用轮询进一步降低中断执行时间。MSPM0G3507 的 DMA 支持外设到内存的传输配置好触发源和传输长度后ADC 每次转换完成自动搬数据中断里直接读内存数组就行。这个方案适合多路采样或者高采样率场景但配置复杂度高新手先把轮询版本跑通再说。验证 PID 效果最直接的方法是用串口打印设定值和反馈值画成曲线看超调量和调节时间。我习惯在中断里每 10 次采样往串口发一次数据波特率 115200用上位机画图。注意串口发送不能放在中断里会阻塞我一般设一个标志位主循环里发。调参时先把 Kp 调到系统刚好不振荡再慢慢加 Ki 消除静差Kd 最后加而且一定要配合滤波。这套流程我在某高校实验室的温控模块上跑了三个月从 20 度升到 80 度超调控制在 2 度以内调节时间 30 秒左右。希望帮到你。本文还有配套的精品资源点击获取