2026 M-Design设计竞赛-基于TI MSPM0G3507微控制器实现的步进电机控制系统
该项目使用了MSPM0G3507,实现了步进电机控制系统的设计,它的主要功能为:UART串口与系统交互,实现命令控制、状态查询、软件清零和开环回零等功能。
标签
电机控制
MSPM0G3507
2026 M-Design
柏树林天邪座葡萄
更新2026-06-09
6

一、项目介绍和创意介绍

本项目是一个基于TI MSPM0G3507微控制器实现的步进电机控制系统,面向2026 M-Design设计竞赛方向三工业控制的电机控制赛道进行设计与实现。项目的核心目标并不是简单地让步进电机旋转,而是围绕电机控制系统这一主题,搭建一个具备基本工程属性的控制平台,使其具备可控、可调、可展示、可扩展的特点。

项目使用42步进电机作为执行机构,配合DRV8825步进驱动模块,通过STEP/DIR方式实现电机运动控制。主控采用MSPM0G3507,使用外部8MHz晶振并结合PLL工作,从而提升系统运行主频和整体响应能力。显示部分采用0.96I2C接口OLED,用于实时显示当前时钟状态、运行状态、角度和速度信息。上位机则通过UART串口与系统交互,实现命令控制、状态查询、软件清零和开环回零等功能。

项目的创意在于:在比赛原型阶段就尽量采用更接近工程的电机控制结构。系统没有停留在“GPIO翻转让电机动起来的最简单层面,而是进一步采用“PWM直接输出STEP脉冲 + 1ms定时器中断更新梯形加减速的控制方案。这样一来,STEP脉冲输出由硬件定时器完成,软件主要负责速度曲线更新和任务管理,在结构上更接近真实工程设计,也更便于后续扩展到双轴云台或二维绘图等应用场景。

二、硬件介绍

本项目使用到的硬件主要包括以下部分。

第一,MSPM0G3507微控制器,作为系统主控,负责时钟初始化、串口交互、状态显示以及步进运动任务管理。

第二,DRV8825步进驱动模块,用于将主控发出的STEP/DIR信号转换为实际驱动步进电机绕组的电流控制信号。

第三,42步进电机,步距角为1.8度,每圈200整步,在1/32细分模式下整圈为6400微步。

第四,0.96I2C OLED,用于显示系统运行状态。

第五,UART串口接口,用于和上位机串口工具通信。

另外,系统时钟使用外部8MHz晶振作为高频参考,并通过PLL将系统提升到更高主频运行,以保证串口交互、OLED刷新和运动控制的整体性能。硬件连接关系中,PA17用于输出STEP脉冲,DIR用于控制旋转方向,SLPRST用于控制驱动器休眠和复位,DCY用于控制衰减模式。

三、方案框图和项目设计思路介绍

系统框图:

image.png

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


四、软件流程图和关键代码介绍

软件流程图:

image.png

关键代码主要集中在三个模块。

第一是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);
}

此外,项目还实现了多个控制命令,包括helpstatstopzerohome以及mvzero表示软件清零,即把当前位置直接设为零;home则表示开环回零,即根据当前软件记录的角度,自动反向运行回到零位;mv表示带参数的运动命令,可以设置角度、方向、起始速度、最大速度和加减速段长度。

五、功能展示图及说明

首先介绍一下实物连接:

本项目当前采用开发板和外设模块连接的方式完成,没有单独绘制完整PCB。MSPM0G3507OLED通过I2C连接,与DRV8825通过STEPDIRSLPRSTDCY等控制线连接,与上位机通过UART接口连接。DRV8825再通过A+A-B+B-四线连接42步进电机。

image.png

项目的功能展示可以分为以下几个部分。

第一,系统上电后OLED显示正常,包括Hello欢迎信息、当前时钟模式、运动状态和当前角度速度。

image.png

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

image.png

第三,输入stat,可以查看当前系统时钟、软件位置、角度、速度、运行状态和忙闲状态。

第四,输入例如mv 360 cw 1 6 20这样的命令后,步进电机会按照设定的方向和梯形加减速方式平滑运行。

第五,输入stop可以立即停止当前运动。

第六,输入zero可以在当前机械位置不动的前提下,将软件位置计数清零。

第七,输入home则会根据当前软件记录的位置开环反向返回零位。

由于无法插入视频,具体的功能测试,请看项目演示视频。

六、设计中遇到的难题和解决方法

本项目在设计和调试过程中遇到了多个难题。

第一个难题是外部晶振和PLL场景下系统启动不稳定。最初在使用内部时钟时程序可以自动运行,但切换到外部晶振后,出现了烧录后不自动运行、必须手动复位的情况。后来即使切回内部晶振,部分场景下仍然会受到影响。

为解决这个问题,项目对时钟初始化流程进行了重构。通过增加对HFXTPLL状态位的等待、增加晶振和PLL的稳定时间、必要时重试PLL启动,并在失败时回退到内部SYSOSC,从而最终实现了基于外部8MHz晶振和PLL的稳定启动。

第二个难题是步进电机控制方式的演进。项目最初使用GPIO加延时的方式直接生成STEP脉冲,这种方式虽然能让电机转动,但会严重阻塞主循环,无法同时做好串口交互和状态显示。后来演进为定时器中断驱动的非阻塞状态机,但进一步测试发现仍然需要更稳定的脉冲输出方式,于是最终改为PWM硬件直接输出STEP

第三个难题是运动控制和位置记录之间的关系。由于项目当前没有原点开关和编码器,系统的位置记录只能依赖于软件对发出STEP数量的累积计数,因此只能实现软件清零和开环回零,不能实现真正意义上的物理硬件回零。对此,项目在功能设计上做了清晰区分:zero表示软件清零,home表示开环回零,并在文档中明确指出它们的适用范围和局限性。


七、对本次竞赛的心得体会

通过本次竞赛项目,我最大的体会是:在项目初期,我的目标只是把步进电机驱动起来,但随着项目推进,我逐渐意识到,一个完整的控制系统必须考虑时钟稳定、指令交互、状态显示、加减速策略、软件结构以及未来扩展性。

这次项目让我从最初的简单驱动,逐步走到具备一定工程结构的步进电机控制平台。我不仅完成了功能,还对底层原理和结构设计有了更深入的理解。

附件下载
step.zip
工程代码压缩包
团队介绍
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号