From bc7a9aacc58486f6fdd1bfc24492c03045e3b2ea Mon Sep 17 00:00:00 2001 From: eddyem Date: Thu, 30 Apr 2020 01:18:39 +0300 Subject: [PATCH] Fix error in CAN speed calculation --- F0-nolib/CANbus_stepper/src/can.c | 2 +- F0-nolib/CANbus_stepper/src/canstepper.bin | Bin 13776 -> 16036 bytes F0-nolib/CANbus_stepper/src/flash.c | 13 +- F0-nolib/CANbus_stepper/src/flash.h | 10 +- F0-nolib/CANbus_stepper/src/hardware.c | 20 +- F0-nolib/CANbus_stepper/src/hardware.h | 18 +- F0-nolib/CANbus_stepper/src/main.c | 8 +- F0-nolib/CANbus_stepper/src/proto.c | 200 ++++++++++++++--- F0-nolib/CANbus_stepper/src/steppers.c | 242 ++++++++++++++++++++- F0-nolib/CANbus_stepper/src/steppers.h | 47 +++- F0-nolib/usbcdc/can.c | 2 +- F0-nolib/usbcdc/proto.c | 7 +- F0-nolib/usbcdc/usbcan.bin | Bin 13456 -> 13468 bytes 13 files changed, 524 insertions(+), 45 deletions(-) diff --git a/F0-nolib/CANbus_stepper/src/can.c b/F0-nolib/CANbus_stepper/src/can.c index 5439343..150de17 100644 --- a/F0-nolib/CANbus_stepper/src/can.c +++ b/F0-nolib/CANbus_stepper/src/can.c @@ -145,7 +145,7 @@ void CAN_setup(uint16_t speed){ CAN->MCR &=~ CAN_MCR_SLEEP; /* (3) */ CAN->MCR |= CAN_MCR_ABOM; /* allow automatically bus-off */ - CAN->BTR |= 2 << 20 | 3 << 16 | (6000/speed) << 0; /* (4) */ + CAN->BTR |= 2 << 20 | 3 << 16 | (6000/speed - 1); /* (4) */ CAN->MCR &=~ CAN_MCR_INRQ; /* (5) */ tmout = 16000000; while((CAN->MSR & CAN_MSR_INAK)==CAN_MSR_INAK) if(--tmout == 0) break; /* (6) */ diff --git a/F0-nolib/CANbus_stepper/src/canstepper.bin b/F0-nolib/CANbus_stepper/src/canstepper.bin index 2233ef357832b247420849e20c648d9a780bf735..b2c1abe7c9e30b703aac60bedd0611fbb536b405 100755 GIT binary patch delta 7808 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 LK>AHWDbRldj<|+y delta 5517 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 diff --git a/F0-nolib/CANbus_stepper/src/flash.c b/F0-nolib/CANbus_stepper/src/flash.c index d86e29a..2104943 100644 --- a/F0-nolib/CANbus_stepper/src/flash.c +++ b/F0-nolib/CANbus_stepper/src/flash.c @@ -46,9 +46,13 @@ static uint32_t maxCnum = FLASH_BLOCK_SIZE / sizeof(user_conf); #define USERCONF_INITIALIZER { \ .userconf_sz = sizeof(user_conf) \ - ,.defflags = 0 \ + ,.defflags.reverse = 0 \ ,.CANspeed = 100 \ ,.driver_type = DRV_NONE \ + ,.microsteps = 16 \ + ,.accdecsteps = 100 \ + ,.motspd = 10 \ + ,.maxsteps = 50000 \ } static int erase_flash(const void*, const void*); @@ -214,7 +218,6 @@ static int erase_flash(const void *start, const void *end){ void dump_userconf(){ SEND("userconf_addr="); printuhex((uint32_t)Flash_Data); SEND("\nuserconf_sz="); printu(the_conf.userconf_sz); - SEND("\nflags="); printuhex(the_conf.defflags); SEND("\nCANspeed="); printu(the_conf.CANspeed); SEND("\ndriver_type="); const char *p = "NONE"; @@ -230,6 +233,12 @@ void dump_userconf(){ break; } SEND(p); + SEND("\nmicrosteps="); printu(the_conf.microsteps); + SEND("\naccdecsteps="); printu(the_conf.accdecsteps); + SEND("\nmotspd="); printu(the_conf.motspd); + SEND("\nmaxsteps="); printu(the_conf.maxsteps); + //flags + SEND("\nreverse="); bufputchar('0' + the_conf.defflags.reverse); newline(); sendbuf(); } diff --git a/F0-nolib/CANbus_stepper/src/flash.h b/F0-nolib/CANbus_stepper/src/flash.h index 4e57c5c..4f03c15 100644 --- a/F0-nolib/CANbus_stepper/src/flash.h +++ b/F0-nolib/CANbus_stepper/src/flash.h @@ -30,13 +30,21 @@ #define FLASH_SIZE_REG ((uint32_t)0x1FFFF7CC) #define FLASH_SIZE *((uint16_t*)FLASH_SIZE_REG) +typedef struct{ + uint8_t reverse : 1; +} defflags_t; + /* * struct to save user configurations */ typedef struct __attribute__((packed, aligned(4))){ + uint32_t maxsteps; // maximal amount of steps from ESW0 to EWS3 uint16_t userconf_sz; // "magick number" uint16_t CANspeed; // default CAN speed - uint8_t defflags; // default flags + uint16_t microsteps; // microsteps amount per step + uint16_t accdecsteps; // amount of steps need for full acceleration/deceleration cycle + uint16_t motspd; // max motor speed ([3000 / motspd] steps per second) + defflags_t defflags; // default flags uint8_t driver_type; // user's settings: type of stepper's driver } user_conf; diff --git a/F0-nolib/CANbus_stepper/src/hardware.c b/F0-nolib/CANbus_stepper/src/hardware.c index ed7a0ac..92d927f 100644 --- a/F0-nolib/CANbus_stepper/src/hardware.c +++ b/F0-nolib/CANbus_stepper/src/hardware.c @@ -18,6 +18,8 @@ #include "hardware.h" +TIM_TypeDef *TIMx = TIM15; // stepper's timer + static uint8_t brdADDR = 0; void Jump2Boot(){ @@ -81,7 +83,7 @@ void gpio_setup(){ | RCC_AHBENR_DMAEN; // setup pins need @start: Vio_ON (PF0, opendrain), ~FAULT (PF1, floating IN), // ~SLEEP (PC15, pushpull), DIR (PA4, pushpull), ~EN (PC13, pushpull) - // ~CS, microstepping2, (PC14, pushpull + // ~CS, microstepping2, (PC14, pushpull) // PA8 - Tx/Rx // PB12..15 - board address, pullup input; PB0..2, PB10 - ESW, pullup inputs (inverse) VIO_OFF(); @@ -115,6 +117,22 @@ void gpio_setup(){ brdADDR = READ_BRD_ADDR(); } +// PA3 (STEP): TIM15_CH2; 48MHz -> 48kHz +void timer_setup(){ + RCC->APB2ENR |= RCC_APB2ENR_TIM15EN; // enable clocking + TIM15->CR1 &= ~TIM_CR1_CEN; // turn off timer + TIM15->CCMR1 = TIM_CCMR1_OC2M_2; // Force inactive + TIM15->PSC = 999; + TIM15->CCER = TIM_CCER_CC2E; + TIM15->CCR1 = 1; // very short pulse + TIM15->ARR = 1000; + // enable IRQ & update values + TIM15->EGR = TIM_EGR_UG; + TIM15->DIER = TIM_DIER_CC2IE; + NVIC_EnableIRQ(TIM15_IRQn); + NVIC_SetPriority(TIM15_IRQn, 0); +} + uint8_t refreshBRDaddr(){ return (brdADDR = READ_BRD_ADDR()); } diff --git a/F0-nolib/CANbus_stepper/src/hardware.h b/F0-nolib/CANbus_stepper/src/hardware.h index 2c87c39..bf434d3 100644 --- a/F0-nolib/CANbus_stepper/src/hardware.h +++ b/F0-nolib/CANbus_stepper/src/hardware.h @@ -68,14 +68,28 @@ #define RESET_UST2() do{GPIOC->BRR = 1<<14;}while(0) #define CS_ACTIVE() do{GPIOC->BRR = 1<<14;}while(0) #define CS_PASSIVE() do{GPIOC->BSRR = 1<<14;}while(0) +// microstepping0 (PA7), 1 (PA5) - set PP +#define UST01_CFG_PP() do{GPIOA->MODER = (GPIOA->MODER & ~(GPIO_MODER_MODER5|GPIO_MODER_MODER7)) | (GPIO_MODER_MODER5_O|GPIO_MODER_MODER7_O);}while(0) +#define SET_UST0() do{GPIOA->BSRR = 1<<7;}while(0) +#define SET_UST1() do{GPIOA->BSRR = 1<<5;}while(0) +#define RESET_UST0() do{GPIOA->BRR = 1<<7;}while(0) +#define RESET_UST1() do{GPIOA->BRR = 1<<5;}while(0) +// end-switches state +#define ESW_STATE() ((GPIOB->IDR & 0x07) | ((GPIOB->IDR>>7) & 0x08)) // configure ~CS as PP output //#define CS_CFG_OUT() do{GPIOC->MODER = (GPIOC->MODER&~GPIO_MODER_MODER14) | GPIO_MODER_MODER14_O; }while(0) // ~CS as floating input -; // Vio_ON, PF0 (inverse) #define VIO_ON() do{GPIOF->BRR = 1;}while(0) #define VIO_OFF() do{GPIOF->BSRR = 1;}while(0) +// turn off timer of STEPS pin +#define STEP_TIMER_OFF() do{TIM15->CR1 &= ~TIM_CR1_CEN;}while(0) + +// timer for stepper +extern TIM_TypeDef *TIMx; +#define timer_isr tim15_isr + extern volatile uint32_t Tms; void Jump2Boot(); @@ -84,4 +98,6 @@ void iwdg_setup(); uint8_t getBRDaddr(); uint8_t refreshBRDaddr(); void sleep(uint16_t ms); +void timer_setup(); + #endif // __HARDWARE_H__ diff --git a/F0-nolib/CANbus_stepper/src/main.c b/F0-nolib/CANbus_stepper/src/main.c index fe31317..aced506 100644 --- a/F0-nolib/CANbus_stepper/src/main.c +++ b/F0-nolib/CANbus_stepper/src/main.c @@ -24,6 +24,7 @@ #include "flash.h" #include "hardware.h" #include "proto.h" +#include "steppers.h" #include "usart.h" #include "usb.h" #include "usb_lib.h" @@ -81,7 +82,7 @@ static char *get_USB(){ } int main(void){ - uint32_t lastT = 0; + uint32_t lastT = 0, ostctr = 0; sysreset(); SysTick_Config(6000, 1); gpio_setup(); // + read board address @@ -120,6 +121,11 @@ int main(void){ } IWDG->KR = IWDG_REFRESH; can_messages_proc(); + if(ostctr != Tms){ // check steppers not more than once in 1ms + ostctr = Tms; + stp_process(); + } + } return 0; } diff --git a/F0-nolib/CANbus_stepper/src/proto.c b/F0-nolib/CANbus_stepper/src/proto.c index a0a3340..cd5c3ca 100644 --- a/F0-nolib/CANbus_stepper/src/proto.c +++ b/F0-nolib/CANbus_stepper/src/proto.c @@ -192,14 +192,45 @@ TRUE_INLINE void userconf_manip(char *txt){ } break; default: - SEND("Wrong argument of userconf manipulation: "); - SEND(txt); + SEND("\nUserconf commands:\n" + "d - userconf dump\n" + "s - userconf store\n" + ); + } +} + +TRUE_INLINE void setdefflags(char *txt){ + const char *needar = "Need argument 0 or 1 for flag"; + txt = omit_spaces(txt); + char ch = *txt; + ++txt; + uint32_t U; + if(txt == getnum(txt, &U)){ + SEND(needar); + return; + } + switch(ch){ + case 'r': + if(U > 1){ + SEND(needar); + return; + } + the_conf.defflags.reverse = U&1; + break; + default: + SEND("\nFlag commands:" + "r - set/clear reverse\n" + ); } } // a set of setters for user_conf TRUE_INLINE void setters(char *txt){ uint32_t U; + uint8_t u8; + const char *drvshould = "Driver type should be one of: 2130, 4988, 8825"; + const char *usshould = "Microsteps amount is a power of two: 1..512"; + const char *motspdshould = "Motor speed should be from 2 to " STR(0xffff/LOWEST_SPEED_DIV); txt = omit_spaces(txt); if(!*txt){ SEND("Setters need more arguments"); @@ -207,6 +238,21 @@ TRUE_INLINE void setters(char *txt){ } char *nxt = getnum(txt + 1, &U); switch(*txt){ + case 'a': // accdecsteps + if(nxt == txt + 1){ + SEND("No accdecsteps value given"); + return; + } + if(U < ACCDECSTEPS_MIN || U > ACCDECSTEPS_MAX){ + SEND("The value should be from" STR(ACCDECSTEPS_MIN) " to " STR(ACCDECSTEPS_MAX)); + return; + } + if(the_conf.accdecsteps != (uint16_t) U){ + the_conf.accdecsteps = (uint16_t) U; + userconf_changed = 1; + SEND("Set accdecsteps to "); printu(U); + } + break; case 'c': // set CAN speed if(nxt == txt + 1){ SEND("No CAN speed given"); @@ -222,54 +268,144 @@ TRUE_INLINE void setters(char *txt){ } if(the_conf.CANspeed != (uint16_t)U){ the_conf.CANspeed = (uint16_t)U; + SEND("Set CAN speed to "); printu(U); userconf_changed = 1; } break; - default: - SEND("Wrong argument of setters: "); - SEND(txt); - } -} - -TRUE_INLINE void driver_commands(char *txt){ - uint32_t U; - char *nxt; - const char *drvshould = "Driver type should be one of: 2130, 4988, 8825"; - txt = omit_spaces(txt); - if(!*txt){ - SEND("Driver commands need more arguments"); - return; - } - switch(*txt){ - case 'i': // init - initDriver(); - break; - case 's': // set type - nxt = getnum(txt + 1, &U); + case 'd': // set driver type if(nxt == txt+1){ SEND(drvshould); break; } + u8 = DRV_NONE; switch(U){ case 2130: - the_conf.driver_type = DRV_2130; + u8 = DRV_2130; SEND("TMC2130"); break; case 4988: - the_conf.driver_type = DRV_4988; + u8 = DRV_4988; SEND("A4988"); break; case 8825: - the_conf.driver_type = DRV_8825; + u8 = DRV_8825; SEND("DRV8825"); break; default: SEND(drvshould); } + if(the_conf.driver_type != u8){ + the_conf.driver_type = u8; + userconf_changed = 1; + } + break; + case 'F': + setdefflags(txt+1); + break; + case 'm': // microsteps + if(nxt == txt + 1){ // no number + SEND(usshould); break; + } + if(U < 1 || U > getMaxUsteps() || (U & (U-1)) != 0){ // U over of range or not power of two + SEND(usshould); break; + } + if(the_conf.microsteps != (uint16_t)U){ + the_conf.microsteps = (uint16_t)U; + userconf_changed = 1; + SEND("Set microsteps to "); printu(U); + } + break; + case 'M': // maxsteps + if(nxt == txt + 1 || U > INT32_MAX){ + SEND("Enter number from 0 (infinity) to INT32_MAX"); break; + } + if(U != the_conf.maxsteps){ + the_conf.maxsteps = U; + userconf_changed = 1; + } + break; + case 's': // motor speed + if(nxt == txt + 1 || U < 2 || U > (0xffff/LOWEST_SPEED_DIV)){ // no number + SEND(motspdshould); break; + } + if(the_conf.motspd != (uint16_t)U){ + the_conf.motspd = (uint16_t)U; + userconf_changed = 1; + SEND("Set motspd to "); printu(U); + } break; default: - SEND("Wrong argument of driver commands: "); - SEND(txt); + SEND("\nSetters commands:\n" + "a - set accdecsteps\n" + "c - set default CAN speed\n" + "d - set driver type\n" + "F - set flags" + "m - set microsteps\n" + "M - set maxsteps\n" + "s - set motspd\n" + ); + } +} + +TRUE_INLINE void driver_commands(char *txt){ + txt = omit_spaces(txt); + if(!*txt){ + SEND("Driver commands need more arguments"); + return; + } + char cmd = *txt++; + txt = omit_spaces(txt); + uint32_t U; + int8_t sign = 1; + if(*txt == '-'){ + ++txt; + sign = -1; + } + char *nxt = getnum(txt, &U); + stp_status st; + switch(cmd){ + case 'e': + SEND("ESW="); + printu(ESW_STATE()); + break; + case 'i': // init + initDriver(); + break; + case 'm': + if(nxt == txt + 1 || U > (INT32_MAX-1)){ + SEND("Give right steps amount: from -INT32_MAX to INT32_MAX"); + return; + } + if(sign > 0) st = stp_move((int32_t)U); + else st = stp_move(-(int32_t)U); + switch(st){ + case STPS_ACTIVE: + SEND("IsMoving"); + break; + case STPS_ONESW: + SEND("OnEndSwitch"); + break; + case STPS_ZEROMOVE: + SEND("ZeroMove"); + break; + case STPS_TOOBIG: + SEND("TooBigNumber"); + break; + default: + SEND("Move to given steps amount"); + } + break; + case 's': + stp_stop(); + SEND("Stop motor"); + break; + default: + SEND("\nDriver commands:\n" + "e - end-switches state\n" + "i - init stepper driver (8825, 4988, 2130)\n" + "m - move N steps\n" + "s - stop\n" + ); } } @@ -363,18 +499,16 @@ void cmd_parser(char *txt, uint8_t isUSB){ "a - get raw ADC values\n" "b - switch to bootloader\n" "d - dump userconf\n" - "Di - init stepper driver (8825, 4988, 2130)\n" - "Ds - set driver type\n" + "D? - stepper driver commands\n" "g - get board address\n" "j - get MCU temperature\n" "k - get U values\n" "m - start/stop monitoring CAN bus\n" "s - send data over CAN: s ID [byte0..7]\n" + "S? - parameter setters\n" "t - send test sequence over RS-485\n" "T - print current time\n" - "Sc - set default CAN speed\n" - "Ud - userconf dump\n" - "Us - userconf store\n" + "U? - options for user configuration\n" ); break; } diff --git a/F0-nolib/CANbus_stepper/src/steppers.c b/F0-nolib/CANbus_stepper/src/steppers.c index 0a5c1fb..9a21a29 100644 --- a/F0-nolib/CANbus_stepper/src/steppers.c +++ b/F0-nolib/CANbus_stepper/src/steppers.c @@ -22,6 +22,28 @@ static drv_type driver = DRV_NONE; +// maximum number of microsteps per driver +static const uint16_t maxusteps[] = { + [DRV_NONE] = 0, + [DRV_NOTINIT] = 0, + [DRV_MAILF] = 0, + [DRV_8825] = 32, + [DRV_4988] = 16, + [DRV_2130] = 256 +}; + +// amount of steps need for full acceleration/deceleration cycle +#define ACCDECSTEPS (the_conf.accdecsteps) +// amount of microsteps in each step +#define USTEPS (the_conf.microsteps) + +int32_t mot_position = -1; // current position of motor (from zero endswitch, -1 means inactive) +uint32_t steps_left = 0; // amount of steps left +stp_state state = STP_SLEEP;// current state of motor +// ARR register values: low (max speed), high (min speed = 10% from max), step (1/50(hi-lo)) +static uint16_t stplowarr, stphigharr, stpsteparr; +static int8_t dir = 0; // moving direction: -1 (negative) or 1 (positive) + /** * @brief checkDrv - test if driver connected */ @@ -42,12 +64,16 @@ static void checkDrv(){ sleep(2); // Check is ~SLEEP is in air oldstate = SLP_STATE(); +#ifdef EBUG if(oldstate) MSG("SLP=1\n"); else MSG("SLP=0\n"); +#endif SLEEP_OFF(); SLP_CFG_OUT(); // sleep -> 1 sleep(2); SLP_CFG_IN(); sleep(2); +#ifdef EBUG if(SLP_STATE()) MSG("SLP=1\n"); else MSG("SLP=0\n"); +#endif if(SLP_STATE() != oldstate){ MSG("~SLP is in air\n"); if(driver != DRV_2130){ @@ -63,13 +89,17 @@ static void checkDrv(){ EN_CFG_IN(); sleep(2); oldstate = EN_STATE(); +#ifdef EBUG if(oldstate) MSG("EN=1\n"); else MSG("EN=0\n"); +#endif DRV_DISABLE(); // EN->1 EN_CFG_OUT(); sleep(2); EN_CFG_IN(); sleep(2); +#ifdef EBUG if(EN_STATE()) MSG("EN=1\n"); else MSG("EN=0\n"); +#endif if(oldstate != EN_STATE()){ MSG("~EN is in air\n"); driver = DRV_NONE; @@ -86,6 +116,47 @@ ret: #endif } +static drv_type ini2130(){ // init 2130: SPI etc. + if(driver != DRV_2130) return DRV_MAILF; + ; + return DRV_MAILF; +} + +static drv_type ini4988_8825(){ // init 4988 or 8825 + if(driver != DRV_4988 && driver != DRV_8825){ + MSG("Wrong drv\n"); + return DRV_MAILF; + } + if(the_conf.microsteps > maxusteps[driver]){ + SEND("Wrong microstepping settings\n"); + return DRV_MAILF; + } + if(the_conf.microsteps == 0){ + SEND("Configure microstepping first\n"); + return DRV_MAILF; + } + // init microstepping pins and set config + UST01_CFG_PP(); + uint8_t PINS = 0; + if(the_conf.microsteps == 16 && driver == DRV_4988) PINS = 7; // microstepping settings for 4988 in 1/16 differs from 8825 + else PINS = (uint8_t)__builtin_ctz(the_conf.microsteps); +#ifdef EBUG + SEND("Microstep PINS="); + printu(PINS); + newline(); sendbuf(); +#endif + // now PINS is M0..M2 settings + if(PINS & 1) SET_UST0(); else RESET_UST0(); + if(PINS & 2) SET_UST1(); else RESET_UST1(); + if(PINS & 4) SET_UST2(); else RESET_UST2(); + // turn on timer + timer_setup(); + // recalculate defaults + stp_chspd(); + SEND("Init OK\n"); + return driver; +} + /** * @brief initDriver - try to init driver * @return driver type @@ -93,12 +164,179 @@ ret: drv_type initDriver(){ if(driver != DRV_NOTINIT){ // reset all settings MSG("clear GPIO & other setup\n"); + STEP_TIMER_OFF(); // TODO: turn off SPI & timer gpio_setup(); // reset pins control } driver = the_conf.driver_type; checkDrv(); - if(driver == DRV_NONE) return driver; - // TODO: some stuff here + if(driver > DRV_MAX-1) return (driver = DRV_NONE); + MSG("init pins\n"); + switch(driver){ + case DRV_2130: + return ini2130(); + break; + case DRV_4988: + case DRV_8825: + return ini4988_8825(); + break; + default: + SEND("Set driver type in config\n"); + return driver; // bad driver type + } return driver; } + +uint16_t getMaxUsteps(){ + if(driver > DRV_MAX-1) return 0; + return maxusteps[driver]; +} + +void stp_chspd(){ + int i; + for(i = 0; i < 2; ++i){ + uint16_t spd = the_conf.motspd; + stplowarr = spd; + stphigharr = spd * LOWEST_SPEED_DIV; + stpsteparr = (spd * (LOWEST_SPEED_DIV - 1)) / ((uint16_t)ACCDECSTEPS) + 1; + } +} + +// check end-switches for stepper motors +void stp_process(){ + // check end-switches; ESW0&ESW3 stops motor + uint8_t esw = ESW_STATE(); + switch(state){ + case STP_MOVE0: // move towards ESW0 + state = STP_SLEEP; + stp_move(-the_conf.maxsteps); // won't move if the_conf.maxsteps == 0 + break; + case STP_MOVE1: // move towards ESW3 + state = STP_SLEEP; + stp_move(the_conf.maxsteps); + break; + case STP_ACCEL: // @ any move check esw + case STP_DECEL: + case STP_MOVE: + case STP_MVSLOW: + if((esw&1) && dir == -1){ // move through ESW0 + state = STP_STOPZERO; // stop @ end-switch + }else if((esw&8) && dir == 1){ // move through ESW3 + state = STP_STOP; // stop @ ESW3 + } + break; + default: // stopping states - do nothing + break; + } +} + +// move motor to `steps` steps, @return 0 if all OK +stp_status stp_move(int32_t steps){ + if(state != STP_SLEEP && state != STP_MOVE0 && state != STP_MOVE1) return STPS_ACTIVE; + if(steps == 0) + return STPS_ZEROMOVE; + if(the_conf.maxsteps && steps > (int32_t)the_conf.maxsteps) return STPS_TOOBIG; + int8_t d; + if(steps < 0){ + d = -1; + steps = -steps; + }else d = 1; // positive direction + // check end-switches + uint8_t esw = ESW_STATE(); + if(((esw&1) && d == -1) || ((esw&8) && d == 1)) return STPS_ONESW; // can't move through esw + dir = d; + // change value of DIR pin + if(the_conf.defflags.reverse){ + if(d>0) + SET_DIR(); + else + CLEAR_DIR(); + }else{ + if(d>0) + CLEAR_DIR(); + else + SET_DIR(); + } + // turn on driver, EN=0 + DRV_ENABLE(); + steps_left = (uint32_t)steps; + // setup timer & start it + TIMx->ARR = stphigharr; + TIMx->CCMR1 = TIM_CCMR1_OC2M_2 | TIM_CCMR1_OC2M_1; // PWM mode 1: active->inacive, preload enable + TIMx->CR1 |= TIM_CR1_CEN; + if(steps < ACCDECSTEPS*2) state = STP_MVSLOW; // move without acceleration + else state = STP_ACCEL; // move with acceleration + return STPS_ALLOK; +} + +// change ARR value +void stp_chARR(uint32_t val){ + if(val < 2) val = 2; + TIMx->ARR = (uint32_t)val; +} + +void stp_stop(){ // stop motor by demand or @ end-switch + switch(state){ + case STP_SLEEP: + return; + break; + case STP_MOVE0: + case STP_MOVE1: + state = STP_SLEEP; + break; + default: + state = STP_STOP; + } +} + +void timer_isr(){ + static uint16_t ustep = 0; + uint16_t tmp, arrval; + if(USTEPS == ++ustep){ // prevent stop @ not full step + ustep = 0; + if(state == STP_STOPZERO) + mot_position = 0; + else{ + if(0 == --steps_left) state = STP_STOP; + mot_position += dir; + } + }else return; + switch(state){ + case STP_ACCEL: // acceleration phase + arrval = (uint16_t)TIMx->ARR - stpsteparr; + tmp = stplowarr; + if(arrval <= tmp || arrval > stphigharr){ + arrval = tmp; + state = STP_MOVE; // end of acceleration phase + } + TIMx->ARR = arrval; + break; + case STP_DECEL: // deceleration phase + arrval = (uint16_t)TIMx->ARR + stpsteparr; + tmp = stphigharr; + if(arrval >= tmp || arrval < stplowarr){ + arrval = tmp; + state = STP_MVSLOW; // end of deceleration phase, move @ lowest speed + } + TIMx->ARR = arrval; + break; + case STP_MOVE: // moving with constant speed phases + if(steps_left <= ACCDECSTEPS){ + state = STP_DECEL; // change moving status to decelerate + } + break; + case STP_MVSLOW: + // nothing to do here: all done before switch() + break; + default: // STP_STOP, STP_STOPZERO + ustep = 0; + TIMx->CCMR1 = TIM_CCMR1_OC1M_2; // Force inactive + TIMx->CR1 &= ~TIM_CR1_CEN; // stop timer + DRV_DISABLE(); + dir = 0; + steps_left = 0; + state = STP_SLEEP; + break; + } + TIMx->SR = 0; +} diff --git a/F0-nolib/CANbus_stepper/src/steppers.h b/F0-nolib/CANbus_stepper/src/steppers.h index 7fcfb08..9635696 100644 --- a/F0-nolib/CANbus_stepper/src/steppers.h +++ b/F0-nolib/CANbus_stepper/src/steppers.h @@ -21,16 +21,61 @@ #include +// the lowest speed equal to [max speed] / LOWEST_SPEED_DIV +#define LOWEST_SPEED_DIV 30 +// min/max value of ACCDECSTEPS +#define ACCDECSTEPS_MIN 30 +#define ACCDECSTEPS_MAX 2000 + typedef enum{ DRV_NONE, // driver is absent DRV_NOTINIT,// not initialized DRV_MAILF, // mailfunction - no Vdd when Vio_ON activated DRV_8825, // DRV8825 connected DRV_4988, // A4988 connected - DRV_2130 // TMC2130 connected + DRV_2130, // TMC2130 connected + DRV_MAX // amount of records in enum } drv_type; +// stepper states +typedef enum{ + STP_SLEEP, // don't moving + STP_ACCEL, // start moving with acceleration + STP_MOVE, // moving with constant speed + STP_MVSLOW, // moving with slowest constant speed + STP_DECEL, // moving with deceleration + STP_STOP, // stop motor right now (by demand) + STP_STOPZERO, // stop motor and zero its position (on end-switch) + STP_MOVE0, // move towards 0 endswitch (negative direction) + STP_MOVE1, // move towards 1 endswitch (positive direction) +} stp_state; + +typedef enum{ + STPS_ALLOK, // no errors + STPS_ACTIVE, // motor is still moving + STPS_TOOBIG, // amount of steps too big + STPS_ZEROMOVE, // give 0 steps to move + STPS_ONESW // staying on end-switch & try to move further +} stp_status; + +extern int32_t mot_position; +extern uint32_t steps_left; +extern stp_state state; +#define stp_getstate() (state) + drv_type initDriver(); drv_type getDrvType(); +uint16_t getMaxUsteps(); + +void stp_chspd(); +stp_status stp_move(int32_t steps); +void stp_stop(); +void stp_process(); +void stp_chARR(uint32_t val); #endif // STEPPERS_H__ + + + + + diff --git a/F0-nolib/usbcdc/can.c b/F0-nolib/usbcdc/can.c index d4bffb6..c1b4f53 100644 --- a/F0-nolib/usbcdc/can.c +++ b/F0-nolib/usbcdc/can.c @@ -165,7 +165,7 @@ void CAN_setup(uint16_t speed){ CAN->MCR &=~ CAN_MCR_SLEEP; /* (3) */ CAN->MCR |= CAN_MCR_ABOM; /* allow automatically bus-off */ - CAN->BTR |= 2 << 20 | 3 << 16 | (6000/speed) << 0; /* (4) */ + CAN->BTR |= 2 << 20 | 3 << 16 | (6000/speed - 1); /* (4) */ CAN->MCR &=~ CAN_MCR_INRQ; /* (5) */ tmout = 16000000; while((CAN->MSR & CAN_MSR_INAK)==CAN_MSR_INAK) if(--tmout == 0) break; /* (6) */ diff --git a/F0-nolib/usbcdc/proto.c b/F0-nolib/usbcdc/proto.c index 56c2972..4bbd13b 100644 --- a/F0-nolib/usbcdc/proto.c +++ b/F0-nolib/usbcdc/proto.c @@ -544,8 +544,13 @@ void printu(uint32_t val){ void printuhex(uint32_t val){ addtobuf("0x"); uint8_t *ptr = (uint8_t*)&val + 3; - int8_t i, j; + int8_t i, j, z=1; for(i = 0; i < 4; ++i, --ptr){ + if(*ptr == 0){ // omit leading zeros + if(i == 3) z = 0; + if(z) continue; + } + else z = 0; for(j = 1; j > -1; --j){ uint8_t half = (*ptr >> (4*j)) & 0x0f; if(half < 10) bufputchar(half + '0'); diff --git a/F0-nolib/usbcdc/usbcan.bin b/F0-nolib/usbcdc/usbcan.bin index 6e94de4cecc8ffcbd20ba900434638ecec35145e..dafdef477385603b00101e2f60f18ba1109c04c9 100755 GIT binary patch delta 2589 zcmbW3eNa@_6~NEkFWv$vi?Ff_EWBj}7I%?FFhE7R*~JC+iNVp1q?pO#XjvK^sIko! zrvmBJkk+J->r4^s=-77J07)&##F^HNP2y-`o!M=Y7Gh{OimbLeZq@Vw`|*0-3TB%> z;-t*{-o5vnbI-l!oO|!m-+ruJ1sY&)7XWh;xsD8^W_5!&fTcgNKg~Q)iUyD3X7;0P zzT8I|Fs*S;evkl6)xcS!lnm|s{S6HvUP>fCBlqcmFOX4$HjlQu_z3B=7NhsXNUsb*q5@=7+9)*IZ^Zg3ggHEYwbl?0D29yXA?>+Pg+SZcIu?EY4Ro3Nry zKAn+fkR1A%G`A*mQ@_2rskvR-KKv;oyM_vv9w!7#(dDX^B z6EHiF{YXEeL4Jkw^z;CzBvwHXBIP)~h@3{o5ILusJm&TrTIAU_iC5q+(eEluWRm_u z(Lp|;k0t2I9$J#1ReyrEs!1?1Ud1K?$$2c0;GK!56B;F1cjpEESa=t1fIQ5115QFv`5vmmqjo z0Ar`G#gXL(Q-P0lli;VcH&IJE>2PA%V}BPKIMAXD+NqTumfZzpM znxrFs`ef3}q@2zq6>9gGfjNToBZoyWh$d`zP+fAC`g=I%2?6pL&g>GiFNXB8Brk&E9=8OsXxV`c5Mx>Pc`%SXun7s(V0D9*7&3)N-wNV@REwq zIN9*bxXg6dCp{HB3#`RbZWZ{xC<{t+%Jg=Nz`sx(g}i?T45?G&O4FU0?J)yS%;3(H z1r7G0{^P*%X3pWZRg>>1BtENB;K%8ulsdAHrm4!33Zk%P-p7K#e}isOvp@gg%p+k@U(#x4)sisToB- zjE(mO2!<4i$a28f7FgXV^^*-y48GRD&X@FE;^sN}r&N1UUCg>7KnzD6c8@I^Osjn- zubp#*v3g@z^5t~gqQxsRa_ZLrvkf_ugf+(YB4V|rz^-Com=d~kMKv+g<15Y@hQng4 zmAK65$S61F?8I1Eb}TN|pV00!UD|<|-APX47I+#FbGVQGF0H8I>6pB6PQ0XB<_~j+ zovrA>&JM(R;9@pW{>Q@dTpLYE*QTzE$!`P%o?XbB6Zke-mEM@I2yb7@f|bwXuGjYl5x!r>$_`o1d7GO?b5mZPy_RpZ~BH~4+5I-`Dx*ez!uiBs20<#MHH?V&K@9Xl# zNezjWusbw;0{>K4%rFy@7>+s0Jfy;3d)#*V%Dvh5xl6J9?quJm$5$rd@4@Iw9l1{5 z!|z{dV5P3=!lLNq&Dk;Tb!RPBqJcdXSBWz*?QdtFFf_75j*+;mjnWMn4df7gF{5>p&hTlmUIrTFhbIleYX zEz*f3BkxX-{0c%oqg4enX;cT57*I>C46kA2Fx?3!A)%_M1D{hFZ3*{reNTHP0#j3lv{v(*OVf delta 2556 zcmb7`e{5679l*cmX9wrkVq*u}7?bC?Bo1~GhlD0slhumpTap(bZ9qwvR<*lyIHhgU zRxpcBvk|mSM_EH}+Cr!bWm2^@Xk!VqAQB0wh!t(9_S$Mm+YEQ3IBaX>YG^NZY~Q}m zgf#0PMOga0@7?#lclZ5q_nu$tAMV$H5%|x_Ks}4hAcwT(Ajln9{K@T))HerZgWEB? z=JqzZn*6+S(2aAF{~!AitkF}h9p0_?aDuh>HRFf+~jqF zdI;H%gk&(_O(;95wZL5bG_KXmpj0h7y>hkW@ae?;1p^9`y%yAr0USe_2H;y!_96S} z%y&G&&r_tyRbw5rL!Onyds4ZTUrk?$VP0L}e(NoO`02Ic+V*`X$#7uZYOjl5(?Ozk zcq~BbnDpxmRL)0s3Q%5DHfwPPz?l~To~vFJCiM&Br1_C?rQ<&l?qqz*rkx(ob>z)< zyXxFbdNIR8t}}0@+r{dny_va~E35uVR()gIaeBJNx!y-ojG z^nJ3MmS}48*Jq$=KEeZ&JoI7BI?G6gN&baAtaV|=FZ$b8dmk6&`Vp+=MS4_IgJ(X1 z^CD^4b>#??{z;3phWzu{zAM0_8Cs*YkiXHbTC;5=;Sy`wAH%>O;2W0({_4`~F?vvI zsMwqBKN%zFImsy3LS#p5{bnvonxPURzSyHLS@+03Zl#yC9wJihN^{FGtlh07N2{=n z2U#x9RYX3}b10io--?{c$MT_^MBFV!&{GNOR26+{WfO7Hv6WM{qbWI@2HfIIdQ3dg zK8P;!{E1~Q|C}DsS#-gy-Eq0pqaFO)%SQi1wFV2B zq}2u!`8{njw2_}tYN#p!XtB-E&2G~Ksk>*gt`WldXz5Ans+ga523CB+wcWMm5@~7 z@mCk1u1n|*^aw`;VtY;a`z9vUCvL_#ycFa4q!Z6Glh#jkt)5ZkrtxF@pas;86C3$+ zN=p=94;%+@1EsPK_dxm(K+a4QuB#{HI=!!KR(EYaBfnK}dpi~q|1R+UiH+s|;7p%l zO|K&VM&bzI<0R5)bDBUjAM6be{22YxR?F^=LXgDMR&HjyqN63-FmD5 zvd#E4D;N7w-PgM1<8pg^aV(jg7XpydwIZfYiauV&g4DfB6 O_#OSbCn~C*&G{cv1x*0}