Candas1 / Split_Hoverboard_SimpleFOC

Split Hoverboards with C++ SimpleFOC (not yet)
MIT License
7 stars 5 forks source link

running simpleFOC from an interrupt and not the main loop() #10

Open RoboDurden opened 1 year ago

RoboDurden commented 1 year ago

Thanks @Candas1 for adding that ADC_USE_INTERRUPT code to Simple FOC\src\current_sense\hardware_specific\gd32\gd32f130/gd32f130_mcu.cpp.

//#define ADC_USE_INTERRUPT // If defined, will use the End of conversion interrupt of inserted ADC I think this was only for testing and you are going to remove it ?

But i think i can use that code to call motor.loopFOC() ( and motor.move() ) right when the two low-side mofet currents have been sampled (which need to be in sync with the bldc pwm.) I had to set a priority lower then the HallSensor interrupts which use

    nvic_irq_enable(gpio_exti_infor[pinNum].irqNum, EXTI_IRQ_PRIO, EXTI_IRQ_SUBPRIO);
=
    nvic_irq_enable(gpio_exti_infor[pinNum].irqNum, 3, 0);

Then i think, the hall interrupts can interrupt the 300 microseconds long loopFOC() (called nested interrupts = nvic = Nested vector interrupt control)

void* _configureADCLowSide(const void* driver_params, const int pinA, const int pinB, const int pinC)
{
...
  adc_interrupt_flag_clear(ADC_INT_EOIC);
  adc_interrupt_enable(ADC_INT_EOIC);
  nvic_irq_enable(ADC_CMP_IRQn, 3, 3);

At the moment i simply add many extern to the gd32f130_mcu.cpp to call motor.XY and allow OUT2T log output:

unsigned long iAdcMicros = 0;
unsigned long iAdcLast = 0;
unsigned long iAdcMicrosLoop = 0;
unsigned long iAdcMicrosLoopLast = 0;

#include <SimpleFOC.h>
extern BLDCMotor motor;
extern float target;
extern LowPassFilter LPF_target;  //  the higher the longer new values need to take effect

boolean bNoLoopFOC = false;
int iUsLoopFOC = 300;

Then this works nearly as nicely as having motor.loopFOC and motor.move in the main loop():

extern "C" {  
  void ADC_CMP_IRQHandler(void)
  {
    if ( adc_interrupt_flag_get(ADC_INT_EOIC) != RESET)
    {
      adc_interrupt_flag_clear(ADC_INT_EOIC);

      unsigned long iNow = _micros();
      iAdcMicros = iNow - iAdcLast;
      iAdcLast = iNow;

      if (motor.enabled)
      {
        if (bNoLoopFOC && (iNow - iAdcMicrosLoopLast > iUsLoopFOC) ) // 250 us = 4 kHz
        {
          bNoLoopFOC = false;
          motor.loopFOC();
          motor.move(LPF_target(target));
          iAdcMicrosLoop = iNow - iAdcMicrosLoopLast;
          iAdcMicrosLoopLast = iNow;
          bNoLoopFOC = true;
        }
      }
    }
  }
}

The sound of the motor has an additional frequency which i think links to iUsLoopFOC.

And now i can add delayMicroseconds(500); to the main loop() with no effect on motor performance :-)

Current consumption (for 22V and T10) is flucktuating from 0.27 A to 0.3 A for both methods. But with my interrupt driven method , the 0.3 A flickers on my lcd power supply a bit more often than the loop::motor.loopFOC() method does. So the old method is slightly more efficient. Do not know why !

The motor.loopFOC + motor.move takes about 300 micro seconds :-( So a frequency of more then 3 kHz is not possible :-/ That is a bit frustrating considering that the very simple Gen2 block commutation runs at 16 kHz ?

It is okay Candas if you do not help here. I think your focus is on using SimpleFOC with a Gen2 board for a very specific robot task, whereas i want a replacement of the Gen2.x firmware where users can add whatever they want to the main loop().

Maybe @robcazzaro will like to check my ADC_CMP_IRQHandler for thread safty. Is 3,3 the lowest possible interrupt priority ?

RoboDurden commented 1 year ago

Okay i added a nice IrqMotor class extending BLDCMotor:

https://github.com/RoboDurden/Arduino-FOC/blob/master/src/IrqMotor.h

class IrqMotor: public BLDCMotor
{
  public:
    IrqMotor(int pp,  float R = NOT_SET, float KV = NOT_SET, float L = NOT_SET);

    // overriding
    int initFOC() override;
    void loopFOC()  {};
    void move(float target = NOT_SET) override;

    void IrqHandler();

    unsigned long iAdcMicros = 0;
    unsigned long iAdcMicrosLoop = 0;
    int iUsLoopFOC = 300;

  protected:

    unsigned long iAdcLast = 0;
    unsigned long iAdcMicrosLoopLast = 0;
};

https://github.com/RoboDurden/Arduino-FOC/blob/master/src/IrqMotor.cpp

IrqMotor* pIrqMotor = NULL;
boolean bIrqMotorThreadNotRunning = true; // thread safety

IrqMotor::IrqMotor(int pp, float _R, float _KV, float _inductance)
: BLDCMotor(pp, _R, _KV, _inductance)
{
  pIrqMotor = this;
}

int IrqMotor::initFOC() 
{
  int iRet = BLDCMotor::initFOC();
  adc_interrupt_flag_clear(ADC_INT_EOIC);
  adc_interrupt_enable(ADC_INT_EOIC);
  nvic_irq_enable(ADC_CMP_IRQn, 3, 3);
  return iRet;
}

void IrqMotor::move(float new_target)   
{
  if(_isset(new_target)) target = new_target;
}

void IrqMotor::IrqHandler()
{
  unsigned long iNow = _micros();
  iAdcMicros = iNow - iAdcLast;
  iAdcLast = iNow;

  if (enabled)
  {
    if (bIrqMotorThreadNotRunning && (iNow - iAdcMicrosLoopLast > iUsLoopFOC) ) // 250 us = 4 kHz
    {
      bIrqMotorThreadNotRunning = false;

      BLDCMotor::loopFOC();
      BLDCMotor::move(target);
      iAdcMicrosLoop = iNow - iAdcMicrosLoopLast;
      iAdcMicrosLoopLast = iNow;

      bIrqMotorThreadNotRunning = true;
    }
  }
}

extern "C" 
{  
  void ADC_CMP_IRQHandler(void)
  {
    if ( adc_interrupt_flag_get(ADC_INT_EOIC) != RESET)
    {
      adc_interrupt_flag_clear(ADC_INT_EOIC);
      if (pIrqMotor)  pIrqMotor->IrqHandler();
    }
  }
}

And i only have about 1% less performance with no more time restrictions in the loop():

22 Volt
T5 T10 T20 T50

KV [rpm/V]  current [A]
IrqMotor
4.49    0.15
9.17    0.27
9.64    0.28
12.6    1.0

BLDCMotor
4.48    0.15
9,23    0.27
9,75    0.28
13,03   1.2

BLDCMotor + loop()::delayMicroseconds(500);
4.49    0.15
8.89    0.3 :-(
9.28    0.3 :-(
11.33   0.6

It only needs

#define IRQMOTOR
#ifdef IRQMOTOR
  IrqMotor motor = IrqMotor(BLDC_POLE_PAIRS, NOT_SET, NOT_SET, 0.00036858); 
#else
  BLDCMotor motor = BLDCMotor(BLDC_POLE_PAIRS, NOT_SET, NOT_SET, 0.00036858); 
#endif

All the other init and loop code can stay in place: https://github.com/RoboDurden/Split_Hoverboard_SimpleFOC/blob/main/src/main.cpp

@Candas1 where is your _powtwo defined ? I needed to add #define _powtwo(n)(1 << n) to SimpleFOCDrivers\src\voltageGenericVoltageSense.cpp :-/

Ideas welcome !

robcazzaro commented 1 year ago

Maybe @robcazzaro will like to check my ADC_CMP_IRQHandler for thread safty. Is 3,3 the lowest possible interrupt priority ?

From a very quick look at the docs, a priority of 0, 0 should work and be the highest priority for the processor. But I haven't checked if the Arduino library uses priority 0 (should not)

RoboDurden commented 1 year ago

the Hall interrupts run at 3,0 priority! If i let the ADC_INT_EOIC = EndOfInsertedConversion run at 0,0 , the hall interrupts seem to get postponed to when the 300 us loopFOC+move have finished :-( I only get the near loop()::loopFOC() performence when i let it run at lower priority than 3,0. I think that is 3,3. I guess that there are only two bits for

void nvic_irq_enable(uint8_t nvic_irq, uint8_t nvic_irq_pre_priority, uint8_t nvic_irq_sub_priority)

Then 3,3 would be the highest number possible and therefore the lowest priority that allows the hall interrupts (and whatever code users will add) to be nested = interrupt the ADC_CMP_IRQHandler(void) -> IrqMotor::IrqHandler() and be executed immediately :-)

RoboDurden commented 1 year ago

I first tried the wrapping approach like SmoothedSensor. But the loop() code reads element variables like motor.enabled which i can not update when some FocMotor function changes the _wrapped.enabled :-/

I do not think that the simpleFOC community will accept a pull request to add my IrqMotor the the Arduino-FOC. Should i try for a SimpleFOCDrivers/motors/IrqMotor.cpp ? I guess i better move my class to the Split_Hoverboard_simpleFOC repo.

Hopefully Simple FOC\src\current_sense\hardware_specific\gd32\gd32f130\gd32f130_mcu.cpp will make it into the Arduino-FOC main branch :-)

robcazzaro commented 1 year ago

Then 3,3 would be the highest number possible and therefore the lowest priority that allows the hall interrupts (and whatever code users will add) to be nested = interrupt the ADC_CMP_IRQHandler(void) -> IrqMotor::IrqHandler() and be executed immediately :-)

Well, you could change the Hall interrupt to be 0,0, and the ADC to be 0,1 or even 1,0. The system uses negative priority numbers for the non-maskable hardware interrupts, and you have full control over the others. But if there are no other interrupts, 3.0 and 3,3 are de facto equivalent to 0,0 and 0,3...

Candas1 commented 1 year ago

Hi,

I made the interrupt a parameter to keep it in case it's useful. I synched arduino-foc-drivers thinking _powtwo will be released also in the main library, but it seems it's pending. If this PR is accepted, loopFOC should be a bit faster.

They will not accept IRQMotor, it's very GD32 specific, and I am not even sure if they will accept my drivers. There are still many changes pending, arduino-gd32 project is not really active.

I thought about making the gd32 drivers a submodule so it's separated from the library but it seems this can generate other problems.

RoboDurden commented 1 year ago

@robcazzaro

you could change the Hall interrupt to be 0,0,

No, the Hall interrupts use the Arduino-Core attachInterrupt(digitalPinToInterrupt(pinA), doA, CHANGE); which why the HallSensor instantly worked on the GD32. But the nvic priorities are hard coded into the Arduino-Core :-(

and you have full control over the others.

No, i have no control what other interrupt code/libraries end users might want to add. Therefore i need the lowest possible priority.

@Candas1 yes i also think they would never accept it. But they themselves are aware that running loopFOC in the main loop is not ideal. I added an optional TIMER2 interrupt to my IrqMotor which would make it usuful for any platform:

int IrqMotor::initFOC(unsigned int iHz) 
{
  int iRet = BLDCMotor::initFOC();

  if (iHz)
  {
    oTimer.setPeriodTime(iHz, FORMAT_HZ);
    oTimer.attachInterrupt(&timer_cb);
    nvic_irq_enable(TIMER2_IRQn, 3, 3);
    oTimer.start();
  }
  else
  {
    adc_interrupt_flag_clear(ADC_INT_EOIC);
    adc_interrupt_enable(ADC_INT_EOIC);
    nvic_irq_enable(ADC_CMP_IRQn, 3, 3);
  }
  return iRet;
}

If there would be a SetPriority function in the Arduino-Core, this TimerX interrupt might run on any Arduino platform. Perfomance with motor.initFOC(3000); is not bad at all:

22 Volt
T5 T10 T20 T50

IrqMotor:Timer2 with loop()::delayMicroseconds(500);
4.45    0.15
9.05    0.3
9.52    0.28
12.33   1.8

IrqMotor:EOIC with loop()::delayMicroseconds(500);
4.45    0.15
9.08    0.26
9.52    0.28
12.33   1.0

BLDCMotor without loop()::delayMicroseconds(500);
4.46    0.15
9,01    0.27
9,43    0.27
11.70   1.3
Candas1 commented 1 year ago

That's a library so I think they let you run it from wherever you want.

RoboDurden commented 1 year ago

Okay @Candas1 i have moved my class to #include "../include/IrqMotor.h" and now i am back to

lib_deps = 
    koendv/RTT Stream@^1.3.0
    https://github.com/Candas1/Arduino-FOC.git#dev-gd32
    https://github.com/Candas1/Arduino-FOC-drivers.git#dev

Would be nice if you temporarily add this to GenericVoltageSense.cpp

#ifndef _powtwo
    #define _powtwo(n)(1 << n)
#endif

Would also be nice if you push your dev code to the main branches. Because i am quite happy with your work already and now would continue with the I2C-Hoverboard code. You then can happily continue with your dev branches and push to the main branch when the performance gets better :-)

Candas1 commented 1 year ago

I won't be able to work on it next few days. You just have to add it to your local copy

RoboDurden commented 1 year ago

Yes take your time :-) It is just that when my repo is ready for a youtube tutorial, users should download lib_deps from your main branch so you can continue to experiment with your dev branches !

I now already successfully moved the entire simpleFOC code as well as all io code to a new class Hoverboard and the main.cpp is nicley empty and ready for users to add their code:

https://github.com/RoboDurden/Split_Hoverboard_SimpleFOC/blob/main/include/Hoverboard.h https://github.com/RoboDurden/Split_Hoverboard_SimpleFOC/blob/main/src/Hoverboard.cpp

#include "Hoverboard.h"

#ifdef DEBUG_STLINK
  #include <RTTStream.h>
  RTTStream rtt;
#endif

Hoverboard oHoverboard;

float target = 0;
LowPassFilter LPF_target(0.5);  //  the higher the longer new values need to take effect

Commander command = Commander(SERIALDEBUG);
void doTarget(char* cmd) { command.scalar(&target, cmd); }
#ifdef IRQMOTOR
void doUsLoopFoc(char* cmd) { float f; command.scalar(&f, cmd); oHoverboard.motor.iUsLoopFOC = f;}
#endif

unsigned long iTimeSetupFinished = 0;
void setup()
{
  #ifdef DEBUG_UART
    DEBUG_UART.begin(DEBUG_UART_BAUD);
    SimpleFOCDebug::enable(&DEBUG_UART);
    motor.useMonitoring(DEBUG_UART);
  #endif
  //Serial2.begin(DEBUG_UART_BAUD); // when using Serial1 as DEBUG_UART

  #ifdef DEBUG_STLINK
    SimpleFOCDebug::enable(&rtt);
    oHoverboard.motor.useMonitoring(rtt);
  #endif

  OUTN("Split Hoverboards with C++ SimpleFOC :-)")

  if (!oHoverboard.Init())
  {
    OUTN("oHoverboard.Init() failed :-(")
    return;
  }

  // add target command T
  command.add('t', doTarget, "target voltage");
  #ifdef IRQMOTOR
    command.add('f', doUsLoopFoc, "set us of loopFoc");
  #endif

  oHoverboard.Blink(3,oHoverboard.oLedGreen);
  iTimeSetupFinished = millis();
}

unsigned long iTimeLast = 0;
unsigned long iTimeSend = 0;
void loop()
{
  unsigned long iNowMs = _micros();
  unsigned long iLoopMS = iNowMs - iTimeLast;
  iTimeLast = iNowMs;
  unsigned long iNow = millis();

  if (  oHoverboard.Move(LPF_target(target)) )   // return only once TRUE when current calibrating is done
  {
    target = 10;
  }

  command.run();  // user communication

  #ifdef IRQMOTOR
    delayMicroseconds(500);
  #endif

  if (iTimeSend > iNow) return;
  iTimeSend = iNow + 100; // TIME_SEND

  OUT2T("A",oHoverboard.GetDcCurrent())
  OUT2T("hall",oHoverboard.sensor.pulse_diff);
#ifdef IRQMOTOR  
  OUT2T("adc us",oHoverboard.motor.iAdcMicros)
  OUT2T("adc Hz",1000000 / oHoverboard.motor.iAdcMicros)
  OUT2T("iAdcMicrosLoop",oHoverboard.motor.iAdcMicrosLoop)
#endif
  OUT2T(target,oHoverboard.motor.target)
  OUT2T("v",oHoverboard.GetRpmPerVolt() )
  OUT2N("loop us",iLoopMS)
}

Next step could already be to add my https://github.com/RoboDurden/GD32_I2C_Slave :-)

Candas1 commented 1 year ago

I will let you know when the main is updated. Feel free to share this is you think it's good enough, but you will have to support those users, please don't send them to me. I want to focus on Simplefoc improvements.

RoboDurden commented 1 year ago

I am happy to help my users: https://github.com/RoboDurden/Hoverboard-Firmware-Hack-Gen2.x/issues/16 well, happy to try to help..

Candas1 commented 1 year ago

That's a good user I mean, the other users 😂