I'm trying to control an unknown stepper motor with the high-end timer (NHET) module of a Texas Instruments Hercules microcontroller I got a freebee from TI almost a year ago. An unknown stepper motor, a driver board and a Hercules RM57 LaunchPad. The code to run the motor was expected to arrive too (it was an assignment for an internal) but that never materialised. In this blog series I'm trying to program the NHET module so that it sends the right signals to make the stepper step.
In the fourth post I check the PWM signal that the MSP430 generates for the ramp-up, steady and ramp-down stages. And what it does when you ask for a fixed number of steps. |
MSP430 Generates Period-modulated PWM
(somehow this title sounds recursive, I'm open to a better suggestion)
The typical way we deal with PWM is using duty-cycle modulation.
For this application though, where we're trying to ramp up a stepper motor gradually, period modulation is needed.
The image below is a capture of the PWM that the MSP430 timers generate to ramp up the motor from stance,
with 128 pulses per second as start speed, 512 PPS as stable speed and an acceleration rate of 128.
(click the image to see it at a useful size)
You can see that the time between the pulses is decreasing - the motor is speeding up.
The capture doesn't have the full reach from start to end - I ran out of logic analyser memory.
It's good enough to show the ramp-up though.
When the motor starts, the time between two level shifts is 3925 ns. So approximately 7850 ns for a full pulse (a PWM *** has two level shifts).
That matches with the initial setting of 128 PPS (the theoretical value is 7812.5 ns). The timer outputs 127.39 pulses per second at the start.
After 1 second, the time between two pulses is 2006 ns (see the attached spreadsheet). The time for a pulse = 4012 ns. So after a second the MSP430 outputs 249.25 pulses per second.
At this point I give up on the math. I leave it to the reader to check if an increase from 128 to 250 pulses is an increase rate of 128 (because I am old and my brain blocks right now ).
Finally the MSP430 Timer Code
Here are the chunks of code that prime the timers and adjust them during runtime.
Init
void Initialize() { // Setup CLKs // Stop Watchdog Timer WDTCTL = WDTPW | WDTHOLD; // Set DCO to 16MHz DCOCTL = 0x00; DCOCTL = CALDCO_16MHZ; BCSCTL1 = CALBC1_16MHZ; // Set SMCLK to 2MHz BCSCTL2 |= DIVS_3; // ACLK = VLO BCSCTL3 |= LFXT1S_2; // Configure Port Directions and Peripherals as Needed // Configure GPIO P1SEL &= ~(POT | nSLEEP); P1SEL2 &= ~(POT | nSLEEP); P1DIR |= (POT | nSLEEP); P1OUT |= (POT | nSLEEP); P2SEL &= ~(RESET | STEP_AIN1 | DIR_AIN2 | BIN2 | BIN1 | nFAULT | nSTALL); P2SEL2 &= ~(RESET | STEP_AIN1 | DIR_AIN2 | BIN2 | BIN1 | nFAULT | nSTALL); P2DIR |= (RESET | STEP_AIN1 | DIR_AIN2 | BIN2 | BIN1); P2OUT &= ~(RESET | STEP_AIN1 | DIR_AIN2 | BIN2 | BIN1); P2DIR &= ~(nFAULT | nSTALL); P2REN |= (nFAULT | nSTALL); P3DIR |= (BIT0 | BIT1 | BIT2 | BIT3 | BIT4 | BIT5 | BIT6 | BIT7); P3OUT &= ~(BIT0 | BIT1 | BIT2 | BIT3 | BIT4 | BIT5 | BIT6 | BIT7); // Configure SPI // Recommended USCI initialization/re-configure process // 1. Set UCSWRST (BIS.B #UCSWRST,&UCxCTL1) // 2. Initialize all USCI registers with UCSWRST=1 (including UCxCTL1) // 3. Configure ports // 4. Clear UCSWRST via software (BIC.B #UCSWRST,&UCxCTL1) // 5. Enable interrupts (optional) via UCxRXIE and/or UCxTXIE // 1) UCB0CTL1 = UCSWRST; // 2) P2DIR |= CS; P2OUT &= ~CS; P1SEL |= SCLK | SDATO | SDATI; P1SEL2 |= SCLK | SDATO | SDATI; // 3) 3-pin, 8-bit SPI master UCB0CTL0 |= UCCKPH | UCMSB | UCMST | UCSYNC; UCB0CTL1 |= UCSSEL_2; // SMCLK // 4) UCB0CTL1 &= ~UCSWRST; // End SPI Configure // UART Initialization uartInit(); // Enables LPM Interrupts __bis_SR_register(GIE); // GUI Composer Monitor Initialization ClearBufferRelatedParam(); // Set Default GPIO Settings G_nSLEEP = low; G_RESET = low; G_STEP_AIN1 = low; G_DIR_AIN2 = low; G_BIN2 = low; G_BIN1 = low; G_nFAULT = high; G_nSTALL = high; UpdateGPIO(); // Setup Pin for Timer Output P2DIR |= STEP_AIN1; P2SEL |= STEP_AIN1; P2SEL2 &= ~STEP_AIN1; // Load Default Speed Profile Values G_START_STOP_SPEED = DEFAULT_START_STOP_SPEED; G_TARGET_SPEED = DEFAULT_TARGET_SPEED; G_ACCEL_RATE = DEFAULT_ACCEL_RATE; G_TOTAL_NUM_STEPS = DEFAULT_NUM_STEPS; // Set Default Register Settings // CTRL Register G_CTRL_REG.Address = 0x00; G_CTRL_REG.DTIME = 0x03; G_CTRL_REG.ISGAIN = 0x03; G_CTRL_REG.EXSTALL = 0x00; G_CTRL_REG.MODE = 0x03; G_CTRL_REG.RSTEP = 0x00; G_CTRL_REG.RDIR = 0x00; G_CTRL_REG.ENBL = 0x01; // TORQUE Register G_TORQUE_REG.Address = 0x01; G_TORQUE_REG.SIMPLTH = 0x00; G_TORQUE_REG.TORQUE = 0xBA; // OFF Register G_OFF_REG.Address = 0x02; G_OFF_REG.PWMMODE = 0x00; G_OFF_REG.TOFF = 0x30; // BLANK Register G_BLANK_REG.Address = 0x03; G_BLANK_REG.ABT = 0x01; G_BLANK_REG.TBLANK = 0x08; // DECAY Register. G_DECAY_REG.Address = 0x04; G_DECAY_REG.DECMOD = 0x03; G_DECAY_REG.TDECAY = 0x10; // STALL Register G_STALL_REG.Address = 0x05; G_STALL_REG.VDIV = 0x03; G_STALL_REG.SDCNT = 0x03; G_STALL_REG.SDTHR = 0x40; // DRIVE Register G_DRIVE_REG.Address = 0x06; G_DRIVE_REG.IDRIVEP = 0x00; G_DRIVE_REG.IDRIVEN = 0x00; G_DRIVE_REG.TDRIVEP = 0x01; G_DRIVE_REG.TDRIVEN = 0x01; G_DRIVE_REG.OCPDEG = 0x01; G_DRIVE_REG.OCPTH = 0x01; // STATUS Register G_STATUS_REG.Address = 0x07; G_STATUS_REG.STDLAT = 0x00; G_STATUS_REG.STD = 0x00; G_STATUS_REG.UVLO = 0x00; G_STATUS_REG.BPDF = 0x00; G_STATUS_REG.APDF = 0x00; G_STATUS_REG.BOCP = 0x00; G_STATUS_REG.AOCP = 0x00; G_STATUS_REG.OTS = 0x00; WriteAllRegisters(); }
main loop. housekeeping, then sleep until timer wakes us up
while (1) { // Update Functions UpdateGPIO(); UpdateDRV8711Registers(); UpdateFullScaleCurrent(); UpdateStepperMotionProfile(); // Enter LPM and wake up when needed __bis_SR_register(LPM0_bits + GIE); }
Interrupt handling (timers get modified during the interrupts, sleeping microcontroller is woken up)
/****************************Interrupt Service Routines*****************************/ #pragma vector=PORT1_VECTOR, PORT2_VECTOR, ADC10_VECTOR, \ USCIAB0TX_VECTOR, TIMER0_A0_VECTOR, TIMER0_A1_VECTOR, \ COMPARATORA_VECTOR, NMI_VECTOR __interrupt void Trap_ISR(void) {} #pragma vector=TIMER1_A0_VECTOR __interrupt void Timer1_A0(void) { // Update Timer at End of PWM Period if (G_LOAD_CCR_VALS == true) { G_CUR_SPEED = G_CUR_SPEED_TEMP; TA1CCR0 = G_TA1CCR0_TEMP; TA1CCR1 = G_TA1CCR1_TEMP; G_LOAD_CCR_VALS = false; } } #pragma vector=TIMER1_A1_VECTOR __interrupt void Timer1_A1(void) { switch (TA1IV) { case TA1IV_NONE: break; // Vector 0: No Interrupt case TA1IV_TACCR1: // Vector 2: CCR1 CCIFG { // Increment Step Counter G_CUR_NUM_STEPS++; if (G_CUR_NUM_STEPS == G_TOTAL_NUM_STEPS) { __bic_SR_register_on_exit(LPM0_bits); } TA1CCTL1 &= ~CCIFG; break; } case TA1IV_TACCR2: // Vector 4: CCR2 CCIFG { TA1CCTL2 &= ~CCIFG; break; } case TA1IV_6: break; // Vector 6: Reserved CCIFG case TA1IV_8: break; // Vector 8: Reserved CCIFG case TA1IV_TAIFG: // Vector 10: Overflow { TACTL &= ~TAIFG; break; } default: break; } } #pragma vector=WDT_VECTOR __interrupt void WatchDog_Timer(void) { // Signal Main Thread to Calculate Next Speed Value G_ACCEL_FLAG = true; // Wake Up the Main Thread __bic_SR_register_on_exit(LPM0_bits); }
I'm skipping GPIO part. The motor is only stepped here if you bypass the indexer. Not what I'm looking for.
Now the real brains. Calculating and applying the profiles.
void UpdateFullScaleCurrent() { // Calculate the 100% Chopping Current Level if ((G_CTRL_REG.ISGAIN != G_ISGAIN_OLD) || (G_TORQUE_REG.TORQUE != G_TORQUE_OLD)) { // Parse ISGAIN unsigned int temp_isgain; if (G_CTRL_REG.ISGAIN == 0) temp_isgain = 5; else if (G_CTRL_REG.ISGAIN == 1) temp_isgain = 10; else if (G_CTRL_REG.ISGAIN == 2) temp_isgain = 20; else if (G_CTRL_REG.ISGAIN == 3) temp_isgain = 40; else temp_isgain = 5; // Calculate Floating Point Value G_FULL_SCALE_CURRENT = (2.75 * (float)G_TORQUE_REG.TORQUE) / (256 * (float)temp_isgain * 0.05); G_ISGAIN_OLD = G_CTRL_REG.ISGAIN; G_TORQUE_OLD = G_TORQUE_REG.TORQUE; } } void UpdateStepperMotionProfile() { if (G_BYPASS_INDEXER == false) { // Motion Profile State Machine // Speed Profile Motor Start if ((G_SPEED_PROFILE == true) && (G_SPEED_PROFILE_LOCK == false)) { G_MOTOR_STATE = SPD_START; } // Speed Profile Motor Accelerating else if ((G_CUR_SPEED < G_TARGET_SPEED) && (G_SPEED_PROFILE == true) && (G_SPEED_PROFILE_LOCK == true)) { G_MOTOR_STATE = SPD_ACCEL; } // Speed Profile Motor Stable else if ((G_CUR_SPEED == G_TARGET_SPEED) && (G_SPEED_PROFILE == true) && (G_SPEED_PROFILE_LOCK == true)) { G_MOTOR_STATE = SPD_STABLE; } // Speed Profile Motor Decelerating else if ((G_CUR_SPEED > G_START_STOP_SPEED) && (G_SPEED_PROFILE == false) && (G_SPEED_PROFILE_LOCK == true)) { G_MOTOR_STATE = SPD_DECEL; } // Speed Profile Motor Stop else if ((G_CUR_SPEED == G_START_STOP_SPEED) && (G_SPEED_PROFILE == false) && (G_SPEED_PROFILE_LOCK == true)) { G_MOTOR_STATE = SPD_STOP; } // Step Profile Motor Start else if ((G_STEP_PROFILE == true) && (G_STEP_PROFILE_LOCK == false)) { G_MOTOR_STATE = STP_START; } // Step Profile Motor Accelerating else if ((G_CUR_NUM_STEPS < G_STEPS_TO_ACCEL) && (G_STEP_PROFILE == true) && (G_STEP_PROFILE_LOCK == true)) { G_MOTOR_STATE = STP_ACCEL; } // Step Profile Motor Stable else if ((G_CUR_NUM_STEPS < (G_TOTAL_NUM_STEPS - G_STEPS_TO_ACCEL)) && (G_STEP_PROFILE == true) && (G_STEP_PROFILE_LOCK == true)) { G_MOTOR_STATE = STP_STABLE; } // Step Profile Motor Decelerating else if ((G_CUR_NUM_STEPS >= (G_TOTAL_NUM_STEPS - G_STEPS_TO_ACCEL)) && (G_CUR_NUM_STEPS < G_TOTAL_NUM_STEPS) && (G_STEP_PROFILE == true) && (G_STEP_PROFILE_LOCK == true)) { G_MOTOR_STATE = STP_DECEL; } // Step Profile Motor Stop else if ((G_CUR_NUM_STEPS >= G_TOTAL_NUM_STEPS) && (G_STEP_PROFILE == true) && (G_STEP_PROFILE_LOCK == true)) { G_MOTOR_STATE = STP_STOP; } // Step Profile Force Motor Stop else if ((G_STEP_PROFILE == false) && (G_STEP_PROFILE_LOCK == true)) { G_MOTOR_STATE = STP_STOP; } // Motor Hold else { G_MOTOR_STATE = HOLD; } // Speed Profile SpeedProfile(); // Step Profile StepProfile(); } }
void SpeedProfile() { // Motor Start if (G_MOTOR_STATE == SPD_START) { G_SPEED_PROFILE_LOCK = true; // Set the Start Speed (Cannot Overflow the Timer Register) G_CUR_NUM_STEPS = 0; // Value Check if (G_START_STOP_SPEED > G_TARGET_SPEED) { G_START_STOP_SPEED = G_TARGET_SPEED; } G_CUR_SPEED = G_START_STOP_SPEED; TA1CCR0 = (SMCLK_MHz*1000000)/G_START_STOP_SPEED; TA1CCR1 = ((SMCLK_MHz*1000000)/G_START_STOP_SPEED) >> 1; // Determine the Acceleration Increment // Watchdog Timer Fires Every 16.384ms, ~61.035 Interupts/s (Accel Rate Cannot be Less Than 64) // Round Accelerat Rate to Increment of 64 G_ACCEL_RATE = (G_ACCEL_RATE >> 6) << 6; G_SPEED_INCR = G_ACCEL_RATE >> 6; // Set the Timer Output Low TA1CCTL1 = OUTMOD_0; P2OUT &= ~STEP_AIN1; // Setup the Output and Interrupt TA1CCTL0 = CCIE; TA1CCTL1 = OUTMOD_3 | CCIE; // Start the Timer TA1CTL = TASSEL_2 | MC_1 | TACLR; // Setup Watchdog Timer as an Interval Timer for Accel/Decel Updates WDTCTL = WDT_MDLY_32; IE1 |= WDTIE; } // Motor Accelerate else if ((G_MOTOR_STATE == SPD_ACCEL) && (G_ACCEL_FLAG == true)) { G_ACCEL_FLAG = false; // Increase Speed if ((G_CUR_SPEED + G_SPEED_INCR) < G_TARGET_SPEED) { // Calcute Next Speed Value (Cannot Overflow the Timer Register) G_CUR_SPEED_TEMP = G_CUR_SPEED + G_SPEED_INCR; G_TA1CCR0_TEMP = (SMCLK_MHz*1000000)/G_CUR_SPEED_TEMP; G_TA1CCR1_TEMP = ((SMCLK_MHz*1000000)/G_CUR_SPEED_TEMP) >> 1; G_LOAD_CCR_VALS = true; } // Load Target Speed else { // Calcute Target Speed Value (Cannot Overflow the Timer Register) G_CUR_SPEED_TEMP = G_TARGET_SPEED; G_TA1CCR0_TEMP = (SMCLK_MHz*1000000)/G_TARGET_SPEED; G_TA1CCR1_TEMP = ((SMCLK_MHz*1000000)/G_TARGET_SPEED) >> 1; G_LOAD_CCR_VALS = true; } } // Motor Decelerate else if ((G_MOTOR_STATE == SPD_DECEL) && (G_ACCEL_FLAG == true)) { G_ACCEL_FLAG = false; // Decrease Speed if (((G_CUR_SPEED - G_SPEED_INCR) > G_START_STOP_SPEED) && (G_CUR_SPEED > G_SPEED_INCR)) { // Calcute Next Speed Value (Cannot Overflow the Timer Register) G_CUR_SPEED_TEMP = G_CUR_SPEED - G_SPEED_INCR; G_TA1CCR0_TEMP = (SMCLK_MHz*1000000)/G_CUR_SPEED_TEMP; G_TA1CCR1_TEMP = ((SMCLK_MHz*1000000)/G_CUR_SPEED_TEMP) >> 1; G_LOAD_CCR_VALS = true; } // Load Stop Speed else { // Calcute Stop Speed Value (Cannot Overflow the Timer Register) G_CUR_SPEED_TEMP = G_START_STOP_SPEED; G_TA1CCR0_TEMP = (SMCLK_MHz*1000000)/G_START_STOP_SPEED; G_TA1CCR1_TEMP = ((SMCLK_MHz*1000000)/G_START_STOP_SPEED) >> 1; G_LOAD_CCR_VALS = true; } } // Motor Stop else if (G_MOTOR_STATE == SPD_STOP) { // Stop Watchdog Interval Timer WDTCTL = WDTPW | WDTHOLD; IE1 &= ~WDTIE; // Disable Timer TA1CTL = TASSEL_2 | MC_0 | TACLR; G_CUR_SPEED = 0; G_SPEED_PROFILE_LOCK = false; } // Catch All else {} }
void StepProfile() { // Motor Start if (G_MOTOR_STATE == STP_START) { G_STEP_PROFILE_LOCK = true; // Set the Start Speed (Cannot Overflow the Timer Register) G_CUR_NUM_STEPS = 0; // Value Check if (G_START_STOP_SPEED > G_TARGET_SPEED) { G_START_STOP_SPEED = G_TARGET_SPEED; } G_CUR_SPEED = G_START_STOP_SPEED; TA1CCR0 = (SMCLK_MHz*1000000)/G_START_STOP_SPEED; TA1CCR1 = ((SMCLK_MHz*1000000)/G_START_STOP_SPEED) >> 1; // Determine the Acceleration Increment // Watchdog Timer Fires Every 16.384ms, ~61.035 Interupts/s (Accel Rate Cannot be Less Than 64) // Round Accelerat Rate to Increment of 64 G_ACCEL_RATE = (G_ACCEL_RATE >> 6) << 6; G_SPEED_INCR = G_ACCEL_RATE >> 6; // Determine the Number of Steps to Accel/Decel // TODO explain this float time = (float)(G_TARGET_SPEED - G_START_STOP_SPEED)/(float)G_ACCEL_RATE; G_STEPS_TO_ACCEL = ((G_ACCEL_RATE >> 1) * (time * time)) + (G_START_STOP_SPEED * time); if (G_STEPS_TO_ACCEL > (G_TOTAL_NUM_STEPS >> 1)) { G_STEPS_TO_ACCEL = G_TOTAL_NUM_STEPS >> 1; } // Set the Timer Output Low TA1CCTL1 = OUTMOD_0; P2OUT &= ~STEP_AIN1; // Setup the Output and Interrupt TA1CCTL0 = CCIE; TA1CCTL1 = OUTMOD_3 | CCIE; // Start the Timer TA1CTL = TASSEL_2 | MC_1 | TACLR; // Setup Watchdog Timer as an Interval Timer for Accel/Decel Updates WDTCTL = WDT_MDLY_32; IE1 |= WDTIE; } // Motor Accelerate else if ((G_MOTOR_STATE == STP_ACCEL) && (G_ACCEL_FLAG == true)) { G_ACCEL_FLAG = false; // Increase Speed if ((G_CUR_SPEED + G_SPEED_INCR) < G_TARGET_SPEED) { // Calcute Next Speed Value (Cannot Overflow the Timer Register) G_CUR_SPEED_TEMP = G_CUR_SPEED + G_SPEED_INCR; G_TA1CCR0_TEMP = (SMCLK_MHz*1000000)/G_CUR_SPEED_TEMP; G_TA1CCR1_TEMP = ((SMCLK_MHz*1000000)/G_CUR_SPEED_TEMP) >> 1; G_LOAD_CCR_VALS = true; } // Load Target Speed else { // Calcute Target Speed Value (Cannot Overflow the Timer Register) G_CUR_SPEED_TEMP = G_TARGET_SPEED; G_TA1CCR0_TEMP = (SMCLK_MHz*1000000)/G_TARGET_SPEED; G_TA1CCR1_TEMP = ((SMCLK_MHz*1000000)/G_TARGET_SPEED) >> 1; G_LOAD_CCR_VALS = true; } } // Motor Decelerate else if ((G_MOTOR_STATE == STP_DECEL) && (G_ACCEL_FLAG == true)) { G_ACCEL_FLAG = false; // Decrease Speed if (((G_CUR_SPEED - G_SPEED_INCR) > G_START_STOP_SPEED) && (G_CUR_SPEED > G_SPEED_INCR)) { // Calcute Next Speed Value (Cannot Overflow the Timer Register) G_CUR_SPEED_TEMP = G_CUR_SPEED - G_SPEED_INCR; G_TA1CCR0_TEMP = (SMCLK_MHz*1000000)/G_CUR_SPEED_TEMP; G_TA1CCR1_TEMP = ((SMCLK_MHz*1000000)/G_CUR_SPEED_TEMP) >> 1; G_LOAD_CCR_VALS = true; } // Load Stop Speed else { // Calcute Stop Speed Value (Cannot Overflow the Timer Register) G_CUR_SPEED_TEMP = G_START_STOP_SPEED; G_TA1CCR0_TEMP = (SMCLK_MHz*1000000)/G_START_STOP_SPEED; G_TA1CCR1_TEMP = ((SMCLK_MHz*1000000)/G_START_STOP_SPEED) >> 1; G_LOAD_CCR_VALS = true; } } // Motor Stop else if (G_MOTOR_STATE == STP_STOP) { // Stop Watchdog Interval Timer WDTCTL = WDTPW | WDTHOLD; IE1 &= ~WDTIE; // Disable Timer TA1CTL = TASSEL_2 | MC_0 | TACLR; // End Step Profle G_CUR_SPEED = 0; G_STEP_PROFILE = false; G_STEP_PROFILE_LOCK = false; } // Catch All else {} }
All code replicated here in line with (check the demo source files for the full extend of) the license:
/* --COPYRIGHT--,BSD * Copyright (c) 2013, Texas Instruments Incorporated * All rights reserved.
I wouldn't mind if an MSP430 timer guru jumps in and writes up a pseudo code version of what's happening here.
Top Comments