一、项目介绍和创意介绍
本项目是一个基于TI MSPM0G3507微控制器实现的步进电机控制系统,面向2026 M-Design设计竞赛方向三“工业控制的电机控制”赛道进行设计与实现。项目的核心目标并不是简单地让步进电机旋转,而是围绕“电机控制系统”这一主题,搭建一个具备基本工程属性的控制平台,使其具备可控、可调、可展示、可扩展的特点。
项目使用42步进电机作为执行机构,配合DRV8825步进驱动模块,通过STEP/DIR方式实现电机运动控制。主控采用MSPM0G3507,使用外部8MHz晶振并结合PLL工作,从而提升系统运行主频和整体响应能力。显示部分采用0.96寸I2C接口OLED,用于实时显示当前时钟状态、运行状态、角度和速度信息。上位机则通过UART串口与系统交互,实现命令控制、状态查询、软件清零和开环回零等功能。
项目的创意在于:在比赛原型阶段就尽量采用更接近工程的电机控制结构。系统没有停留在“GPIO翻转让电机动起来”的最简单层面,而是进一步采用“PWM直接输出STEP脉冲 + 1ms定时器中断更新梯形加减速”的控制方案。这样一来,STEP脉冲输出由硬件定时器完成,软件主要负责速度曲线更新和任务管理,在结构上更接近真实工程设计,也更便于后续扩展到双轴云台或二维绘图等应用场景。
二、硬件介绍
本项目使用到的硬件主要包括以下部分。
第一,MSPM0G3507微控制器,作为系统主控,负责时钟初始化、串口交互、状态显示以及步进运动任务管理。
第二,DRV8825步进驱动模块,用于将主控发出的STEP/DIR信号转换为实际驱动步进电机绕组的电流控制信号。
第三,42步进电机,步距角为1.8度,每圈200整步,在1/32细分模式下整圈为6400微步。
第四,0.96寸I2C OLED,用于显示系统运行状态。
第五,UART串口接口,用于和上位机串口工具通信。
另外,系统时钟使用外部8MHz晶振作为高频参考,并通过PLL将系统提升到更高主频运行,以保证串口交互、OLED刷新和运动控制的整体性能。硬件连接关系中,PA17用于输出STEP脉冲,DIR用于控制旋转方向,SLP和RST用于控制驱动器休眠和复位,DCY用于控制衰减模式。
三、方案框图和项目设计思路介绍
系统框图:

在设计思路上,最初项目是用软件直接控制GPIO模拟STEP脉冲,这种方式实现简单,但运动过程会阻塞主循环,不利于串口交互和显示。后来逐步演进到定时器中断驱动的非阻塞结构,最终又进一步改成PWM硬件直接输出STEP脉冲。这样一来,硬件定时器负责稳定脉冲输出,1ms定时器负责更新频率和运动状态,大大提高了系统的结构合理性。
四、软件流程图和关键代码介绍
软件流程图:

关键代码主要集中在三个模块。
第一是bsp_clock_init模块,用于重写默认时钟初始化流程。由于项目使用外部晶振和PLL,直接依赖工具自动生成的初始化代码可能在下载、复位、上电等场景下表现不稳定,因此在代码中增加了等待状态位、稳定延时和必要重试机制,保证系统能可靠进入工作状态。
#define CLOCK_SYSOSC_FREQ_HZ 32000000U
#define CLOCK_SYSPLL_FREQ_HZ 80000000U
#define CLOCK_HFXT_STARTUP_STEPS 8U
#define CLOCK_HFXT_TIMEOUT_US 20000U
#define CLOCK_SYSPLL_TIMEOUT_US 5000U
#define CLOCK_SYSPLL_RETRY_COUNT 5U
#define CLOCK_HFXT_SETTLE_MS 2U
#define CLOCK_PLL_SETTLE_MS 2U
volatile uint32_t g_runtime_mclk_hz = CLOCK_SYSOSC_FREQ_HZ;
volatile ClockRuntimeMode g_runtime_clock_mode = CLOCK_RUNTIME_SYSOSC_32M;
/* 这里配置的是:
* HFXT = 8MHz
* SYSPLL 输出 = 80MHz
* 作为 CPU 最终运行时钟 */
static const DL_SYSCTL_SYSPLLConfig g_clockSYSPLLConfig = {
.inputFreq = DL_SYSCTL_SYSPLL_INPUT_FREQ_8_16_MHZ,
.rDivClk2x = 0,
.rDivClk1 = 0,
.rDivClk0 = 0,
.enableCLK2x = DL_SYSCTL_SYSPLL_CLK2X_DISABLE,
.enableCLK1 = DL_SYSCTL_SYSPLL_CLK1_DISABLE,
.enableCLK0 = DL_SYSCTL_SYSPLL_CLK0_ENABLE,
.sysPLLMCLK = DL_SYSCTL_SYSPLL_MCLK_CLK0,
.sysPLLRef = DL_SYSCTL_SYSPLL_REF_HFCLK,
.qDiv = 19,
.pDiv = DL_SYSCTL_SYSPLL_PDIV_1,
};
static bool Clock_waitStatus(uint32_t mask, uint32_t expected, uint32_t timeoutUs)
{
/* 轮询等待指定状态位,避免外设/时钟在异常状态下死循环。 */
while (timeoutUs-- != 0U) {
if ((DL_SYSCTL_getClockStatus() & mask) == expected) {
return true;
}
delay_cycles(32);
}
return false;
}
static void Clock_switchMCLKToSYSOSC(void)
{
/* 在切时钟前,先把 MCLK 收回到内部 SYSOSC,
* 这样后面对 HFXT / SYSPLL 的关断和重配更安全。 */
switch (DL_SYSCTL_getMCLKSource()) {
case DL_SYSCTL_MCLK_SOURCE_HSCLK:
DL_SYSCTL_switchMCLKfromHSCLKtoSYSOSC();
break;
case DL_SYSCTL_MCLK_SOURCE_LFCLK:
DL_SYSCTL_switchMCLKfromLFCLKtoSYSOSC();
break;
default:
break;
}
}
static bool Clock_configSYSPLLWithTimeout(const DL_SYSCTL_SYSPLLConfig *config)
{
uint32_t ctlTemp;
/* 重新配置 PLL 前,先确保它已经彻底处于 OFF 状态。 */
DL_SYSCTL_disableSYSPLL();
if (!Clock_waitStatus(DL_SYSCTL_CLK_STATUS_SYSPLL_OFF,
DL_SYSCTL_CLK_STATUS_SYSPLL_OFF, CLOCK_SYSPLL_TIMEOUT_US)) {
return false;
}
DL_Common_updateReg(&SYSCTL->SOCLOCK.SYSPLLCFG0, (uint32_t) config->sysPLLRef,
SYSCTL_SYSPLLCFG0_SYSPLLREF_MASK);
DL_Common_updateReg(&SYSCTL->SOCLOCK.SYSPLLCFG1, (uint32_t) config->pDiv,
SYSCTL_SYSPLLCFG1_PDIV_MASK);
/* SYSPLLPARAM0/1 由 TI 提供的工厂参数决定,
* 这里按输入频率加载对应参数。 */
ctlTemp = DL_CORE_getInstructionConfig();
DL_CORE_configInstruction(DL_CORE_PREFETCH_ENABLED, DL_CORE_CACHE_DISABLED,
DL_CORE_LITERAL_CACHE_ENABLED);
SYSCTL->SOCLOCK.SYSPLLPARAM0 =
*(volatile uint32_t *) ((uint32_t) config->inputFreq);
SYSCTL->SOCLOCK.SYSPLLPARAM1 =
*(volatile uint32_t *) ((uint32_t) config->inputFreq + (uint32_t) 0x4);
CPUSS->CTL = ctlTemp;
DL_Common_updateReg(&SYSCTL->SOCLOCK.SYSPLLCFG1,
((config->qDiv << SYSCTL_SYSPLLCFG1_QDIV_OFS) &
SYSCTL_SYSPLLCFG1_QDIV_MASK),
SYSCTL_SYSPLLCFG1_QDIV_MASK);
DL_Common_updateReg(&SYSCTL->SOCLOCK.SYSPLLCFG0,
(((config->rDivClk2x << SYSCTL_SYSPLLCFG0_RDIVCLK2X_OFS) &
SYSCTL_SYSPLLCFG0_RDIVCLK2X_MASK) |
((config->rDivClk1 << SYSCTL_SYSPLLCFG0_RDIVCLK1_OFS) &
SYSCTL_SYSPLLCFG0_RDIVCLK1_MASK) |
((config->rDivClk0 << SYSCTL_SYSPLLCFG0_RDIVCLK0_OFS) &
SYSCTL_SYSPLLCFG0_RDIVCLK0_MASK) |
config->enableCLK2x | config->enableCLK1 | config->enableCLK0 |
(uint32_t) config->sysPLLMCLK),
(SYSCTL_SYSPLLCFG0_RDIVCLK2X_MASK | SYSCTL_SYSPLLCFG0_RDIVCLK1_MASK |
SYSCTL_SYSPLLCFG0_RDIVCLK0_MASK |
SYSCTL_SYSPLLCFG0_ENABLECLK2X_MASK |
SYSCTL_SYSPLLCFG0_ENABLECLK1_MASK |
SYSCTL_SYSPLLCFG0_ENABLECLK0_MASK |
SYSCTL_SYSPLLCFG0_MCLK2XVCO_MASK));
/* 完成参数写入后使能 SYSPLL,并等待 GOOD。 */
DL_SYSCTL_enableSYSPLL();
return Clock_waitStatus(DL_SYSCTL_CLK_STATUS_SYSPLL_GOOD,
DL_SYSCTL_CLK_STATUS_SYSPLL_GOOD, CLOCK_SYSPLL_TIMEOUT_US);
}
const char *Clock_GetStatusText(void)
{
/* 这里只区分“最终 CPU 跑在 PLL 80M”还是“回退到 SYSOSC 32M”。 */
if (g_runtime_clock_mode == CLOCK_RUNTIME_SYSPLL_80M) {
return "CLK:PLL 80M";
}
return "CLK:SYS 32M";
}
void SYSCFG_DL_SYSCTL_init(void)
{
uint32_t retry;
bool pllReady = false;
/* 默认先按 SYSOSC 32MHz 认为系统可运行。
* 后面如果 HFXT/PLL 成功,再切到 80MHz。 */
delay_set_cpuclk_hz(CLOCK_SYSOSC_FREQ_HZ);
g_runtime_mclk_hz = CLOCK_SYSOSC_FREQ_HZ;
g_runtime_clock_mode = CLOCK_RUNTIME_SYSOSC_32M;
/* 先准备内部时钟和基本等待状态。 */
DL_SYSCTL_setBORThreshold(DL_SYSCTL_BOR_THRESHOLD_LEVEL_0);
DL_SYSCTL_setFlashWaitState(DL_SYSCTL_FLASH_WAIT_STATE_2);
DL_SYSCTL_setSYSOSCFreq(DL_SYSCTL_SYSOSC_FREQ_BASE);
Clock_switchMCLKToSYSOSC();
DL_SYSCTL_setMCLKDivider(DL_SYSCTL_MCLK_DIVIDER_DISABLE);
DL_SYSCTL_setULPCLKDivider(DL_SYSCTL_ULPCLK_DIV_1);
/* 先把 HFXT/PLL 关干净,再重新启动。 */
DL_SYSCTL_disableHFCLKStartupMonitor();
DL_SYSCTL_disableSYSPLL();
if (!Clock_waitStatus(DL_SYSCTL_CLK_STATUS_SYSPLL_OFF,
DL_SYSCTL_CLK_STATUS_SYSPLL_OFF, CLOCK_SYSPLL_TIMEOUT_US)) {
return;
}
DL_SYSCTL_disableHFXT();
if (!Clock_waitStatus(DL_SYSCTL_CLK_STATUS_HFCLK_OFF,
DL_SYSCTL_CLK_STATUS_HFCLK_OFF, CLOCK_HFXT_TIMEOUT_US)) {
return;
}
/* 配置并启动 HFXT,依赖 startup monitor 等待 GOOD。 */
DL_SYSCTL_setHFXTFrequencyRange(DL_SYSCTL_HFXT_RANGE_4_8_MHZ);
DL_SYSCTL_setHFXTStartupTime(CLOCK_HFXT_STARTUP_STEPS);
SYSCTL->SOCLOCK.HSCLKEN &= ~(SYSCTL_HSCLKEN_USEEXTHFCLK_MASK);
SYSCTL->SOCLOCK.HSCLKEN |= SYSCTL_HSCLKEN_HFXTEN_ENABLE;
DL_SYSCTL_enableHFCLKStartupMonitor();
if (!Clock_waitStatus(DL_SYSCTL_CLK_STATUS_HFCLK_GOOD,
DL_SYSCTL_CLK_STATUS_HFCLK_GOOD, CLOCK_HFXT_TIMEOUT_US)) {
DL_SYSCTL_disableHFCLKStartupMonitor();
DL_SYSCTL_disableHFXT();
return;
}
/* GOOD 只代表“已经能用”,工程上再补一点稳定时间更稳。 */
delay_cycles((CLOCK_SYSOSC_FREQ_HZ / 1000U) * CLOCK_HFXT_SETTLE_MS);
/* PLL 启动可能受晶振起振边沿、上电时序等影响,
* 因此这里允许重试若干次。 */
for (retry = 0; retry < CLOCK_SYSPLL_RETRY_COUNT; retry++) {
if (!Clock_configSYSPLLWithTimeout(&g_clockSYSPLLConfig)) {
continue;
}
/* PLL GOOD 后再补一小段稳定时间,再做最终检查。 */
delay_cycles((CLOCK_SYSOSC_FREQ_HZ / 1000U) * CLOCK_PLL_SETTLE_MS);
if (SYSCFG_DL_SYSCTL_SYSPLL_init()) {
pllReady = true;
break;
}
}
/* 如果 PLL 最终没起来,就保底回退到 SYSOSC,保证程序仍可运行。 */
if (!pllReady) {
DL_SYSCTL_disableSYSPLL();
delay_set_cpuclk_hz(CLOCK_SYSOSC_FREQ_HZ);
g_runtime_mclk_hz = CLOCK_SYSOSC_FREQ_HZ;
g_runtime_clock_mode = CLOCK_RUNTIME_SYSOSC_32M;
return;
}
/* PLL 正常后,最终把 MCLK 切到 HSCLK(SYSPLL)。 */
DL_SYSCTL_setMCLKSource(SYSOSC, HSCLK, DL_SYSCTL_HSCLK_SOURCE_SYSPLL);
delay_set_cpuclk_hz(CLOCK_SYSPLL_FREQ_HZ);
g_runtime_mclk_hz = CLOCK_SYSPLL_FREQ_HZ;
g_runtime_clock_mode = CLOCK_RUNTIME_SYSPLL_80M;
}
第二是bsp_uart模块。该模块采用“UART接收中断收字节 + 主循环按行取命令”的方式实现。中断中只负责把收到的字节写入环形缓冲区,不在中断里直接解析命令,从而让串口层保持简单和稳定。主循环中调用UART_ReadLine函数,获取一整行命令后,再交由上层命令处理函数解析。
static volatile uint8_t g_uartRxBuffer[UART_RX_BUFFER_SIZE];
static volatile uint16_t g_uartRxHead = 0U;
static volatile uint16_t g_uartRxTail = 0U;
static bool UART_RxPush(uint8_t data)
{
/* 环形缓冲写入。
* 如果缓冲满了,这里直接丢弃新数据,避免覆盖未处理命令。 */
uint16_t next = (uint16_t) ((g_uartRxHead + 1U) % UART_RX_BUFFER_SIZE);
if (next == g_uartRxTail) {
return false;
}
g_uartRxBuffer[g_uartRxHead] = data;
g_uartRxHead = next;
return true;
}
static bool UART_RxPop(uint8_t *data)
{
/* 环形缓冲读取。 */
if (g_uartRxHead == g_uartRxTail) {
return false;
}
*data = g_uartRxBuffer[g_uartRxTail];
g_uartRxTail = (uint16_t) ((g_uartRxTail + 1U) % UART_RX_BUFFER_SIZE);
return true;
}
void UART_Init(void)
{
/* 清空软件接收状态,并打开 UART 中断。 */
g_uartRxHead = 0U;
g_uartRxTail = 0U;
NVIC_EnableIRQ(UART_0_INST_INT_IRQN);
}
void UART_SendChar(char ch)
{
/* 阻塞发送一个字节。
*/
DL_UART_Main_transmitDataBlocking(UART_0_INST, (uint8_t) ch);
}
void UART_SendString(const char *str)
{
/* 逐字节发送 C 字符串。 */
while (*str != '\0') {
UART_SendChar(*str++);
}
}
void UART_SendData(const uint8_t *data, uint16_t length)
{
/* 发送任意字节数组。 */
uint16_t i;
for (i = 0U; i < length; i++) {
DL_UART_Main_transmitDataBlocking(UART_0_INST, data[i]);
}
}
void UART_SendLine(const char *str)
{
/* 发送一行文本,并自动补 \r\n。 */
UART_SendString(str);
UART_SendString("\r\n");
}
bool UART_ReadLine(char *buffer, uint16_t bufferSize)
{
static char lineBuffer[UART_LINE_BUFFER_SIZE];
static uint16_t lineLength = 0U;
uint8_t ch;
/* 逐字节拼接一条命令行。
* 收到 \r 或 \n 就返回一整行给上层解析。 */
while (UART_RxPop(&ch)) {
if (ch == '\r' || ch == '\n') {
/* 空行直接忽略。 */
if (lineLength == 0U) {
continue;
}
lineBuffer[lineLength] = '\0';
strncpy(buffer, lineBuffer, bufferSize - 1U);
buffer[bufferSize - 1U] = '\0';
lineLength = 0U;
return true;
}
/* 行缓冲满了就丢弃当前这一行,等待下一条命令。 */
if (lineLength < (UART_LINE_BUFFER_SIZE - 1U)) {
lineBuffer[lineLength++] = (char) ch;
} else {
lineLength = 0U;
}
}
return false;
}
uint16_t UART_GetRxCount(void)
{
/* 统计环形缓冲里待处理字节数。 */
if (g_uartRxHead >= g_uartRxTail) {
return (uint16_t) (g_uartRxHead - g_uartRxTail);
}
return (uint16_t) (UART_RX_BUFFER_SIZE - g_uartRxTail + g_uartRxHead);
}
void UART_ClearRxBuffer(void)
{
/* 首尾指针都拉回 0。 */
g_uartRxHead = 0U;
g_uartRxTail = 0U;
}
void UART0_IRQHandler(void)
{
/* UART0 接收中断:
* 只负责把 RX FIFO 中的数据搬进软件缓冲。 */
switch (DL_UART_Main_getPendingInterrupt(UART_0_INST)) {
case DL_UART_MAIN_IIDX_RX:
while (!DL_UART_Main_isRXFIFOEmpty(UART_0_INST)) {
(void) UART_RxPush(DL_UART_Main_receiveData(UART_0_INST));
}
break;
default:
break;
}
}
第三是bsp_stepper模块。这个模块是整个项目的核心。系统通过PWM硬件直接输出STEP脉冲,不再由软件手工拉高拉低IO。每1ms的定时器中断负责根据当前运动状态机判断此刻处于加速、巡航还是减速阶段,并重新计算目标步频,然后更新PWM周期。与此同时,PWM周期中断则精确统计已经发出的STEP数量,用于位置计数和到位停机。
/* 速度下限保护,避免后续除法出错。 */
static uint32_t Stepper_ClampStepHz(uint32_t stepHz)
{
if (stepHz < STEPPER_MIN_STEP_HZ) {
return STEPPER_MIN_STEP_HZ;
}
return stepHz;
}
/* 软件位置计数更新。这里不是闭环,只是根据发出的 STEP 数量做位置估算。 */
static void Stepper_UpdatePosition(uint32_t microsteps, StepperDir dir)
{
if (dir == STEPPER_DIR_CCW) {
g_stepperState.positionMicrosteps += (int32_t) microsteps;
} else {
g_stepperState.positionMicrosteps -= (int32_t) microsteps;
}
}
/* 根据当前运动阶段,计算本时刻应该使用的步频。 */
static uint32_t Stepper_GetTrapStepHzInternal(void)
{
uint32_t index = g_stepperMotion.stepIndex;
if (g_stepperMotion.state == STEPPER_RUN_ACCEL &&
g_stepperMotion.accelMicrosteps > 0U) {
return g_stepperMotion.startStepHz +
((g_stepperMotion.maxStepHz - g_stepperMotion.startStepHz) *
(index + 1U)) / g_stepperMotion.accelMicrosteps;
}
if (g_stepperMotion.state == STEPPER_RUN_DECEL &&
g_stepperMotion.decelMicrosteps > 0U) {
uint32_t decelIndex =
g_stepperMotion.totalMicrosteps - g_stepperMotion.stepIndex;
return g_stepperMotion.startStepHz +
((g_stepperMotion.maxStepHz - g_stepperMotion.startStepHz) *
decelIndex) / g_stepperMotion.decelMicrosteps;
}
return g_stepperMotion.maxStepHz;
}
/* 把目标 stepHz 换算成 PWM 周期和高电平宽度,并写入 TIMG7。 */
static void Stepper_ApplyStepFrequency(uint32_t stepHz)
{
uint32_t periodCounts;
uint32_t highCounts;
uint32_t lowCounts;
stepHz = Stepper_ClampStepHz(stepHz);
periodCounts = STEPPER_TIMER_CLK_HZ / stepHz;
if (periodCounts < STEPPER_MIN_PERIOD_COUNTS) {
periodCounts = STEPPER_MIN_PERIOD_COUNTS;
}
/* 对 STEP 驱动器来说,固定一个较短高电平宽度更合理。 */
highCounts = STEPPER_DEFAULT_PULSE_HIGH_COUNTS;
if (highCounts >= periodCounts) {
highCounts = periodCounts / 2U;
}
lowCounts = periodCounts - highCounts;
g_stepperState.stepPulseHighUs = highCounts / 10U;
g_stepperState.stepPulseLowUs = lowCounts / 10U;
/* 使用零点更新,使新频率从下一个周期开始生效,避免中途跳变。 */
DL_TimerG_setLoadValue(PWM_0_INST, periodCounts - 1U);
DL_TimerG_setCaptureCompareValue(PWM_0_INST, highCounts, GPIO_PWM_0_C0_IDX);
}
/* 装载一次新的梯形加减速运动任务。 */
static void Stepper_PrepareTrapMotion(
uint32_t microsteps, StepperDir dir, const StepperTrapProfile *profile)
{
uint32_t accelSteps = profile->accelMicrosteps;
uint32_t decelSteps = profile->decelMicrosteps;
memset(&g_stepperMotion, 0, sizeof(g_stepperMotion));
/* 如果给定的加减速段过长,就自动缩成最短可行三角形轨迹。 */
if ((accelSteps + decelSteps) > microsteps) {
accelSteps = microsteps / 2U;
decelSteps = microsteps - accelSteps;
}
g_stepperMotion.active = true;
g_stepperMotion.dir = dir;
g_stepperMotion.state = (accelSteps > 0U) ? STEPPER_RUN_ACCEL :
((microsteps > decelSteps) ? STEPPER_RUN_CRUISE : STEPPER_RUN_DECEL);
g_stepperMotion.totalMicrosteps = microsteps;
g_stepperMotion.accelMicrosteps = accelSteps;
g_stepperMotion.decelMicrosteps = decelSteps;
g_stepperMotion.cruiseMicrosteps = microsteps - accelSteps - decelSteps;
g_stepperMotion.startStepHz = Stepper_ClampStepHz(profile->startStepHz);
g_stepperMotion.maxStepHz = Stepper_ClampStepHz(profile->maxStepHz);
if (g_stepperMotion.maxStepHz < g_stepperMotion.startStepHz) {
g_stepperMotion.maxStepHz = g_stepperMotion.startStepHz;
}
g_stepperMotion.currentStepHz = g_stepperMotion.startStepHz;
}
/* 每 1ms 由 TIMER_0 中断调用一次,更新当前速度档位。 */
static void Stepper_UpdateProfileByTick(void)
{
if (!g_stepperMotion.active) {
return;
}
if (g_stepperMotion.stepIndex >=
(g_stepperMotion.accelMicrosteps + g_stepperMotion.cruiseMicrosteps)) {
g_stepperMotion.state = STEPPER_RUN_DECEL;
} else if (g_stepperMotion.stepIndex >= g_stepperMotion.accelMicrosteps) {
g_stepperMotion.state = STEPPER_RUN_CRUISE;
} else {
g_stepperMotion.state = STEPPER_RUN_ACCEL;
}
g_stepperMotion.currentStepHz = Stepper_GetTrapStepHzInternal();
Stepper_ApplyStepFrequency(g_stepperMotion.currentStepHz);
}
此外,项目还实现了多个控制命令,包括help、stat、stop、zero、home以及mv。zero表示软件清零,即把当前位置直接设为零;home则表示开环回零,即根据当前软件记录的角度,自动反向运行回到零位;mv表示带参数的运动命令,可以设置角度、方向、起始速度、最大速度和加减速段长度。
五、功能展示图及说明
首先介绍一下实物连接:
本项目当前采用开发板和外设模块连接的方式完成,没有单独绘制完整PCB。MSPM0G3507与OLED通过I2C连接,与DRV8825通过STEP、DIR、SLP、RST、DCY等控制线连接,与上位机通过UART接口连接。DRV8825再通过A+、A-、B+、B-四线连接42步进电机。

项目的功能展示可以分为以下几个部分。
第一,系统上电后OLED显示正常,包括Hello欢迎信息、当前时钟模式、运动状态和当前角度速度。

第二,通过串口输入help,可以查看所有支持命令和参数范围。

第三,输入stat,可以查看当前系统时钟、软件位置、角度、速度、运行状态和忙闲状态。
第四,输入例如mv 360 cw 1 6 20这样的命令后,步进电机会按照设定的方向和梯形加减速方式平滑运行。
第五,输入stop可以立即停止当前运动。
第六,输入zero可以在当前机械位置不动的前提下,将软件位置计数清零。
第七,输入home则会根据当前软件记录的位置开环反向返回零位。
由于无法插入视频,具体的功能测试,请看项目演示视频。
六、设计中遇到的难题和解决方法
本项目在设计和调试过程中遇到了多个难题。
第一个难题是外部晶振和PLL场景下系统启动不稳定。最初在使用内部时钟时程序可以自动运行,但切换到外部晶振后,出现了烧录后不自动运行、必须手动复位的情况。后来即使切回内部晶振,部分场景下仍然会受到影响。
为解决这个问题,项目对时钟初始化流程进行了重构。通过增加对HFXT和PLL状态位的等待、增加晶振和PLL的稳定时间、必要时重试PLL启动,并在失败时回退到内部SYSOSC,从而最终实现了基于外部8MHz晶振和PLL的稳定启动。
第二个难题是步进电机控制方式的演进。项目最初使用GPIO加延时的方式直接生成STEP脉冲,这种方式虽然能让电机转动,但会严重阻塞主循环,无法同时做好串口交互和状态显示。后来演进为定时器中断驱动的非阻塞状态机,但进一步测试发现仍然需要更稳定的脉冲输出方式,于是最终改为PWM硬件直接输出STEP。
第三个难题是运动控制和位置记录之间的关系。由于项目当前没有原点开关和编码器,系统的位置记录只能依赖于软件对发出STEP数量的累积计数,因此只能实现软件清零和开环回零,不能实现真正意义上的物理硬件回零。对此,项目在功能设计上做了清晰区分:zero表示软件清零,home表示开环回零,并在文档中明确指出它们的适用范围和局限性。
七、对本次竞赛的心得体会
通过本次竞赛项目,我最大的体会是:在项目初期,我的目标只是把步进电机驱动起来,但随着项目推进,我逐渐意识到,一个完整的控制系统必须考虑时钟稳定、指令交互、状态显示、加减速策略、软件结构以及未来扩展性。
这次项目让我从最初的简单驱动,逐步走到具备一定工程结构的步进电机控制平台。我不仅完成了功能,还对底层原理和结构设计有了更深入的理解。